Analysis model: gpt-5.5 xhigh
Xmas '91 by The Skyhawks - Technical Dissection
Xmas '91 / tsh_xmas is a 1991 MS-DOS Christmas demo by The Skyhawks. The
Hornet 1991 index lists the archive as Xmas '91 by The Skyhawks, and the
menu banner in the executable calls it The SkyHawks Christmas Demo -91.
Release year: 1991
This is a multi-part packed DOS demo rather than a single executable. A small
selector asks for sound device and computer speed, writes a three-byte config
file, temporarily renames each numbered part to TSH.EXE, runs it as a DOS
child process, and then restores the original filename. The numbered parts are
UNP-packable MZ executables, while .001, .003, and .005 are RASP-tagged
module/sample data files.

The three stills above are decoded directly from the demo's own planar image streams: the eagle page from the intro, a later logo page, and the ending mountain picture. They are not emulator screenshots or redraws.
Front-page media note: the card uses this decoded still sheet directly. I did not make a GIF for this pass because only one composite still sheet is published here, and no full AVI capture was made.
Historical phone/address strings exist in the ending scroller data. They are intentionally omitted here.
Runtime-To-Code Concordance
The still sheet is decoded from internal image streams, not captured from an emulator timeline. The mapping below is therefore a decoded-asset-to-code concordance: it ties the three visible stills to the part chain and renderer paths that would present them during playback, while keeping the no-full-AVI boundary intact.
The selector is the first runtime gate. TSHMAIN.EXE writes the three-byte
TSH.DAT configuration at 00883, then repeatedly renames each numbered
executable to TSH.EXE, launches it with DOS AH=4Bh, and renames it back.
That explains why the still sheet spans several different engines: .000,
.002, .004, and .006 are separate UNP-expanded MZ children, while .001,
.003, and .005 are RASP music/sample data files.
The eagle page in the still sheet maps to part .000. TSH000.EXE selects
BIOS mode 0Dh at 14ac6, uploads the initial palette through 14c46 and
14c59, then decodes the first planar stream from flat 0x0180 into
A000:0000. The decoder at 14b6a consumes the 6-bit RLE format, writes each
40-byte row four times through the Sequencer Map Mask, and advances from plane
1 to 2 to 4 to 8 before moving to the next scanline.
The same .000 part explains why the intro image set can contain several
static-looking pages without storing raw full screens. The first stream expands
to eight 8000-byte pages; the second stream expands to three more. The display
loop at 14ae3..14b0e changes CRTC start address registers 0Ch/0Dh by
0x1f40 bytes per page and waits three retraces per page for the first set.
The later logo pages use the same mechanism with a nine-retrace hold.
The middle logo/page still in the sheet corresponds to the later executable
parts rather than the .000 eagle-only decoder. Part .002 starts in normal
mode 13h at 16504, loads TSH_XMAS.001, installs timer audio, uploads 256
DAC entries and a 64,000-byte frame at 1698c..169e5, and then performs a
vblank-synchronized 32-color DAC update at 1665b..16686. Its six-object
strip blitter clears previous 16-line regions and copies new strips with a
300-byte stride, so the visible logo/sprite movement is a table-driven
mode-13h presentation rather than a planar page flip.
The unchained/tweaked material in part .004 is the VGA-heavy continuation.
It starts from BIOS mode 12h and 13h, rewrites VGA registers into a planar
tweaked layout, uses an 80-byte CRTC offset (0x50), and writes vertical
strips through a 0x00a0 row stride while rotating through planes. That is the
code family behind the card description's unchained VGA and CRTC/page effects,
even though the published visual evidence here is a decoded still sheet rather
than an animation.
The ending mountain still maps to part .006. That part initializes identity
palette state, decodes two planar image streams to A000:0000 and
A000:1f40, and uses the same 6-bit RLE/map-mask family: repeat counts are
byte & 0x3f, and the Sequencer Map Mask advances through planes 1, 2,
4, and 8. The still sheet's mountain image is therefore not a redrawn
illustration; it is a direct product of the part's packed planar image data.
Audio and timing are shared but part-local. .002, .004, and .006 each
read TSH.DAT, load the appropriate RASP file, install a timer interrupt
around 1d195, bias sample bytes by +0x80, and select silence, LPT DAC,
Sound Blaster, or PC speaker output from the config byte. The reliable visual
timing in this pass remains code-derived: .000 has fixed retrace counts, but
the later parts loop until Esc and were not captured as a full AVI timeline.
Sources
- Hornet 1991 demo index: https://ftp.scene.org/mirrors/hornet/demos/1991/00_index.txt
- Scene.org archive: https://ftp.scene.org/mirrors/hornet/demos/1991/tsh_xmas.zip
- Scene.org file page: https://files.scene.org/view/mirrors/hornet/demos/1991/tsh_xmas.zip
- UNP was run under DOSBox-X against local copies of the packed parts.
Archive
The examined archive is tsh_xmas.zip from the Hornet mirror:
tsh_xmas.zip 303658 bytes
ZIP contents:
demosite.com 1709 bytes 1992-11-16 17:28
tsh_xmas.000 45362 bytes 1980-01-02 07:22
tsh_xmas.001 90064 bytes 1980-01-02 21:00
tsh_xmas.002 25345 bytes 1980-01-02 07:25
tsh_xmas.003 112676 bytes 1980-01-02 07:00
tsh_xmas.004 32808 bytes 1980-01-02 07:24
tsh_xmas.005 85578 bytes 1980-01-02 21:00
tsh_xmas.006 21823 bytes 1980-01-02 07:24
tsh_xmas.exe 1787 bytes 1980-01-02 07:25
Most internal timestamps are reset to 1980, so the release year comes from the Hornet index and the demo banner, not from ZIP member dates.
Hashes:
e2df19a395005acc446ceccc5464dae127481372a58ff52beec6627d6bb07cc2 tsh_xmas.zip
533191877cb302b0e7864a88f87e6f962beac8ecb2d5ef4e389dc385ce533b3f tsh_xmas.exe packed
0a08bc82e873f1986bc6ad2078b6fc2a6fbc1156b76a282aa4b70b9a728d73ab tsh_xmas.000 packed
909f48440792266e3689218a2310a6548cfb81eac3a5fbe8a0619d1faafb0946 tsh_xmas.001 data
c7758c744102dea3f2679f9de68ca04496f1be7e3c6263b1528ad41de09b8e4c tsh_xmas.002 packed
cbafc61dc2194fa7d35867997d2f91651248fc5d52080d36ccad5085012c0825 tsh_xmas.003 data
0f25f8ba50de13e47b4e0fc3abaeb99a33d0f3732eb3eba0b4e4558402379a95 tsh_xmas.004 packed
3deb112cecbeae138b28aa3006f89ebdc9931848d419bcd122575e28b9808db0 tsh_xmas.005 data
e3fd1c9cd80c500cc142590f733d8b5ddd78de058cc084daeccf0c360bfc947a tsh_xmas.006 packed
UNP-expanded executable hashes:
e50a61310b8c46c58686ed18a969b126c476ca77d8beba392047e69f910cfbb2 TSHMAIN.EXE
7921debe2f5ac3e5565a6e413762b3b6316fb22ebce5edb1776c01f2e0e7523d TSH000.EXE
c9999534643b059feef18116eb7382d5a459e28ac378efcb971b09c6e63bbb4e TSH002.EXE
93e03b252ce84d35899ea34ec8a7e4972acdac6e361e8a43b4e23531f843f702 TSH004.EXE
b10a53c83128b9fa1d94786f15f81ff1f9135156e8e87ccc9f6a854a895582c6 TSH006.EXE
MZ Layout After UNP
The shipped .000, .002, .004, .006, and .exe files are packed MZs.
UNP expands them to normal MZ executables with larger load images:
file packed expanded load image entry
TSHMAIN.EXE 1787 2243 2211 0000:069E -> 0069E
TSH000.EXE 45362 85185 85137 14A5:006F -> 14ABF
TSH002.EXE 25345 119872 119840 1010:6404 -> 16504
TSH004.EXE 32808 165677 165629 1FB0:1B6F -> 2166F
TSH006.EXE 21823 50849 50801 0493:1078 -> 059A8
The nonzero code segments matter. When TSH000.EXE writes cs:000d, that is
not load offset 0x000d; it is flat load offset 0x14a5d. The RLE decoder
parameters, palettes, and file names all live in the part's code segment.
Selector And Part Chain
tsh_xmas.exe is the frontend. Its useful strings are plaintext after UNP:
Tsh.Dat
TSH_XMAS.000
TSH_XMAS.001
TSH_XMAS.002
TSH_XMAS.003
TSH_XMAS.004
TSH_XMAS.005
TSH_XMAS.006
TSH.EXE
At 0069e it first releases memory with int 21h, AH=4Ah, probes available
memory with AH=48h, BX=FFFFh, and requires about 0x61a8 paragraphs. It then
opens each required file name in a seven-entry loop:
006c0 mov cx,0007
006c5 mov dx,000d ; first TSH_XMAS.xxx string
006c8 mov ax,3d00h ; open existing
006cb int 21h
006cf jae ok
; print missing-file message, exit
006df mov ax,3e00h ; close
006e2 int 21h
006e4 add dx,000d ; next fixed-size name slot
006e7 loop 006c8
The sound menu stores two bytes:
byte 0 device
0 = silence
1 = LPT DAC
2 = Sound Blaster
3 = PC speaker
byte 1 LPT port selector when the device is LPT
1 = LPT1, 2 = LPT2
The speed menu stores byte 2:
1 -> slowest selected class
2
3
4
5 -> fastest selected class
00883 writes exactly three bytes, from cs:000a, to Tsh.Dat:
00883 mov ax,4000h
00886 mov bx,[cs:0008] ; config file handle
0088b mov cx,0003
00892 mov dx,000a
00895 int 21h
Each numbered visual part then opens TSH.DAT at startup and reads these same
three bytes. For example, TSH002.EXE opens TSH.DAT at 169fd, reads three
bytes to cs:519d at 169e8, and closes it at 16a0e.
The selector uses a rename/execute/rename-back pattern:
00789 mov dx,0068 ; "TSH_XMAS.000"
0078c mov di,0075 ; "TSH.EXE"
0078f call 00842 ; int 21h AH=56h rename .000 -> TSH.EXE
00792 mov ax,007d ; DOS parameter block
00799 mov dx,0075 ; run TSH.EXE
0079c call 00847 ; int 21h AH=4Bh EXEC
007a3 mov dx,0075
007a8 mov di,0068
007ab call 00842 ; rename back
It repeats the same sequence for .002, .004, and .006. The odd-numbered
files are data files and are not executed.
Effect Timeline
No full AVI capture was made for this pass. The reliable timings below are
code-derived from retrace loops and the usual VGA 70.086 Hz mode timing. Later
parts are intentionally open-ended: the code loops until Esc is detected from
keyboard port 60h.
time after setup part event
0:00.00 .000 mode 0Dh set, first packed planar page stream decoded
0:00.00 to 0:00.34 .000 8 CRTC pages shown, 3 retraces per page
0:00.34 to 0:00.73 .000 3 logo pages shown, 9 retraces per page
0:00.73 to 0:02.16 .000 final hold, 100 retraces, then child exits
user-controlled .002 mode 13h part loops until Esc
user-controlled .004 tweaked unchained VGA part loops until Esc
user-controlled .006 ending/mountain/scroller part loops until Esc
The absolute wall-clock time from pressing the final setup key to the first
visible .000 page depends on unpacked code speed, disk speed, and DOSBox
cycles because the image pages are decoded before the timed page-flip loop.
Part .000: Planar Intro And Page Flip
TSH000.EXE is the most compact self-contained graphics part. It uses BIOS
mode 0Dh and writes directly into A000h planar memory.
Startup:
14ac4 int 21h, AH=4Ah ; resize memory block
14ac6 mov ax,000dh
14ac9 int 10h ; 320x200 16-color planar mode
14acb call 14c46 ; identity EGA palette via int 10h AX=1000h
14ace call 14c59 ; write initial 16-color DAC palette
The first packed stream starts at relative segment 0010, offset 0080.
Because the MZ is loaded relative to the program load segment, this maps to
flat load offset 0x0180. It decodes:
source 0010:0080 -> flat 0x0180
groups 0x0640 = 1600 groups = 8 pages * 200 rows
row width 0x0028 = 40 bytes per plane row
destination A000:0000
decode end flat 0x9a87
The second stream starts immediately after that region:
source 09a8:0080 -> flat 0x9b00
groups 0x0258 = 600 groups = 3 pages * 200 rows
row width 0x0028 = 40 bytes per plane row
destination A000:0000
decode end flat 0x14a55, just before the code segment data
That exact end address is an important sanity check. The compressed picture streams fill almost the entire pre-code load image.
RLE Format
The decoder at 14b6a is byte-oriented:
if (byte & 0xc0) != 0xc0:
output byte
else:
count = byte & 0x3f
value = next byte
output value count times
The subtle point is that a repeat run may cross a row boundary or a plane boundary. The assembly does not clip a repeat to the current row. It keeps the repeat counter live while the destination state advances.
Core literal path:
14ba6 lodsb
14bb8 and al,c0h
14bbc je repeat
14bbf mov es:[bp],al
14bc3 inc bp
14bc4 cmp bp,di
14bc6 jb 14ba6
Core repeat path:
14bdf and al,3fh ; low six bits are repeat count
14be1 mov ah,al
14be3 lodsb ; repeated value
14bf4 mov bl,al
14bf7 mov cl,ah
14bf9 xor ch,ch
14bfb mov es:[bp],bl
14bff inc bp
14c00 cmp bp,di
14c02 jb same_span
; advance plane/row while still inside the repeat
14c1d loop 14bfb
bp is the current address inside A000h. di is the end address for the
current 40-byte span. When a span ends, the decoder switches VGA planes instead
of simply continuing linearly:
14bc8 sub bp,[cs:000d] ; return to row start
14bcd inc byte [cs:000a]; plane number 1,2,3,4
14bd2 call 14c2a ; set Sequencer Map Mask
14bd5 cmp byte [cs:000a],04h
14bdb ja next_row
The map-mask helper translates plane number to Sequencer index 2 masks:
14c2a mov al,02h ; Sequencer Map Mask register
14c2c mov ah,[cs:000a] ; 1,2,3,4
14c31 cmp ah,03h
14c36 mov ah,04h ; plane 3 -> mask 4
14c3a cmp ah,04h
14c3f mov ah,08h ; plane 4 -> mask 8
14c41 mov dx,03c4h
14c44 out dx,ax
So one 40-byte row is written four times at the same A000h addresses, once per plane. After the fourth plane, the decoder advances by 40 bytes and starts the next scanline. After 200 scanlines it has one 8000-byte planar page. The first stream stores eight such pages back-to-back.
CRTC Page Flip
After decoding the first stream, .000 shows each 8000-byte page by changing
CRTC start address registers 0Ch/0Dh:
14ae3 xor bx,bx
14ae6 mov cx,0008 ; eight pages
14ae9 mov dx,03dah
; wait for three vertical retraces
14afd mov dx,03d4h
14b00 mov ah,bl
14b02 mov al,0dh
14b04 out dx,ax ; low start address
14b05 mov ah,bh
14b07 mov al,0ch
14b09 out dx,ax ; high start address
14b0a add bx,1f40h ; next 8000-byte page
14b0e loop 14ae9
0x1f40 is 8000 decimal, exactly 40 bytes * 200 rows. In mode 0Dh, that is
one 320x200 16-color page per plane address space.
The second visible loop uses the same CRTC page flip but waits nine retraces
between pages and shows three logo pages. The part then waits 100 more retraces
and exits with int 21h, AX=4C00h.
RASP Data Files
The data files .001, .003, and .005 are not pictures. They are module and
sample banks for the shared playback engine embedded in .002, .004, and
.006.
Each starts with a mostly zero header and has the signature at file offset
0x438:
offset 0x438: 52 41 53 50 "RASP"
The loader also accepts FLT4 and FLT8, because the executable contains the
expected signatures next to each other:
RASPFLT4FLT8
The load path is:
1d239 open data file
1d24a read 0x043c bytes into header buffer
1d25e compare header[0x438] against RASP/FLT4/FLT8
1d2a4 RASP path: pattern/order table starts at header+0x3b8
1d2b8 scan 0x80 order bytes for maximum pattern number
1d2c8 pattern_bytes = (max + 1) << 10
1d2d0 allocate pattern buffer with int 21h AH=48h
1d2e8 read pattern data
1d301 loop 31 sample headers
1d328 allocate each nonzero sample
1d340 read sample data
1d35b add 0x80 to every sample byte
The last operation is important. Samples are stored as signed-ish centered bytes on disk, then biased into unsigned 8-bit form for the playback backends:
1d350 xor si,si
1d353 xor di,di
1d356 mov cx,[sample_length]
1d35b lodsb
1d35c add al,80h
1d35e stosb
1d35f loop 1d35b
If PC-speaker output is selected, the data is transformed once more through a lookup table:
1d361 cmp byte [device],03h
1d367 jne no_pc_speaker_map
1d374 push bx
1d375 mov bx,aa1ah
1d378 lodsb
1d379 xlat cs:[bx]
1d37b stosb
1d37c loop 1d378
The resource sizes account for the whole files:
file signature max order pattern bytes samples sample bytes total check
tsh_xmas.001 RASP 8 9216 10 79764 0x43c + 9216 + 79764 = 90064
tsh_xmas.003 RASP 22 23552 14 88040 0x43c + 23552 + 88040 = 112676
tsh_xmas.005 RASP 9 10240 9 74254 0x43c + 10240 + 74254 = 85578
So the RASP interpretation is byte-complete for all three data files.
Shared Audio Engine
The same playback engine appears in .002, .004, and .006, relocated to
different offsets. In .002, the entry points are:
1cfbe initialize mixer/player state
1cfb4 open/read/parse RASP data file
1d195 install timer interrupt
1d1f4 restore timer interrupt
1cfb0 free allocated buffers
Device selection comes from TSH.DAT byte 0:
0 silence
1 LPT DAC
2 Sound Blaster
3 PC speaker
For LPT output, the engine uses BIOS data-area ports:
1d09f cmp byte [lpt_selector],01h
1d0a7 mov ax,es:[0408h] ; LPT1 base
1d0af cmp byte [lpt_selector],02h
1d0b7 mov ax,es:[040ah] ; LPT2 base
For Sound Blaster, it probes DSP reset/status by walking likely base ports:
1d0c7 mov dx,0216h
1d0ca mov al,01h
1d0cc out dx,al
; delay
1d0d5 out dx,al ; reset low
1d0d9 add dx,0004h
1d0dc in al,dx
1d0e0 cmp al,0aah ; DSP ready signature
1d0e6 add dx,0010h ; try next base
1d0eb add dx,0006h ; DSP write port
1d0f8 mov al,0d1h
1d0fa out dx,al ; speaker on
For PC speaker, it saves port 61h, enables speaker/gate bits, and later
restores both port 61h and PIT channel 2:
1d1ce in al,61h
1d1d0 mov [saved_61h],al
1d1d4 or al,03h
1d1d6 out 61h,al
...
1d224 mov al,[saved_61h]
1d228 out 61h,al
1d22b mov al,0b0h
1d22d out 43h,al
1d22f out 42h,0ffh
1d235 out 42h,0ffh
Timer installation rewrites INT 08 and PIT channel 0:
1d195 mov ax,3508h
1d198 int 21h ; save old INT 08
1d1db mov ax,2508h
1d1de int 21h ; install selected playback ISR
1d1e1 mov bx,[timer_divisor]
1d1e6 mov al,36h
1d1e8 out 43h,al
1d1ec out 40h,al ; low divisor
1d1f0 out 40h,al ; high divisor
The speed menu changes the divisor before installation. For .002:
16522 read speed byte from TSH.DAT
1652a speed 1 -> divisor 0x00c6
16535 speed 2 -> divisor 0x0077
16540 speed 3 -> divisor 0x0058
1654b speed 4 -> divisor 0x0044
16556 speed 5 -> divisor 0x0036
The fastest setting therefore drives the player interrupt hardest.
Part .002: Mode 13h Logo/Sprite Loop
.002 uses normal 320x200 256-color mode 13h:
16504 mov ax,0013h
16507 int 10h
16509 call open_config
1650c call read_3_config_bytes
1650f call close_config
1655d call init_player
16577 mov dx,5188h ; "TSH_XMAS.001"
1657a call load_rasp
16586 call install_timer_audio
1658c int 21h AX=3509h ; save keyboard interrupt
1659f int 21h AX=2509h ; install local keyboard handler
The local keyboard handler watches scancode 1 from port 60h, acknowledges
the keyboard controller at port 61h, and sends EOI to the PIC:
1696f pusha
16970 in al,60h
16972 cmp al,01h
16976 mov byte [esc_flag],01h
1697c in al,61h
16980 out 61h,al ; strobe high
16984 out 61h,al ; strobe low
16986 mov al,20h
16988 out 20h,al
1698b iret
Palette And Frame Upload
1698c copies BIOS/DOS palette-like state from segment 40h, then builds two
working palette ramps. The first frame upload writes DAC entries and then copies
64000 bytes to A000h:
169c9 mov cx,0100h
169cc mov dx,03c8h
169cf mov ax,cx
169d6 lodsb
169d9 out dx,ax ; index via 3c8/3c9 in word form
169db lodsb
169dc out dx,al
169dd lodsb
169de out dx,al
169e2 mov cx,7d00h
169e5 rep movsw ; 64000 bytes to A000h
The later per-frame palette update is vblank-synchronized:
1665b mov dx,03dah
1665e in al,dx
1665f test al,08h
16663 in al,dx
16664 test al,08h
1667f mov dx,03c8h
16682 mov al,50h
16684 out dx,al
16686 rep outsb ; 0x60 bytes, 32 RGB entries
The inner loop is not just a passive wait. Every third pass it scrolls the palette source pointer backward by three bytes:
16668 cmp byte [palette_phase],03h
16670 sub word [palette_offset],0003h
16678 mov word [palette_offset],0060h ; wrap
That is the color-cycling mechanism.
Six-Object Stripe Blitter
The main visual work is a six-object blitter. It keeps six current and previous screen offsets, clears previous 16-line strips, then copies new 16-line strips.
Clear old strips:
1689a mov es,A000h
168a2 mov bp,0006h
168a7 mov di,[old_position + bx]
168ac cmp [source_pointer + bx],3340h ; sentinel
168b5 mov cx,0010h
168b8 stosw * 10 ; 20 bytes
168c2 add di,012ch ; next screen row: 300 bytes apart
168c6 loop 168b8
Draw new strips:
168d8 mov di,[new_position + bx]
168dd mov si,[source_pointer + bx]
168e8 mov cx,0010h
168eb movsw * 10 ; copy 20 bytes
168f5 add di,012ch
168f9 loop 168eb
The 0x012c stride is 300 decimal, so this is not a normal full-width row
copy. It is copying small vertical sprites/tiles into precomputed screen
positions. The six source pointers are fed from a table at 6044h, translated
through a table at 61fbh, then multiplied by 0x0140:
1676b al = table[bp + 6044h]
1678a xlat cs:[61fbh]
1678c mul 0140h
1678e store source offset
Text/Glyph Writer
The glyph helper at 1692e draws one character into a rolling 30-column area:
16941 reset countdown to 0x1f
1694b dec countdown
16950 di = (0x1e - countdown) << 3
1695b add di,70a8h
1695f mov dx,0008h ; 8 rows
16962 mov cx,0007h ; 7 bytes per row
16965 rep movsb
16967 add di,0139h
1696b dec dx
1696c jne next_row
This is a compact font stamp: 8 rows, 7 bytes per row, with a nonstandard row
stride. The text source at 5233h is advanced one byte at a time and mapped
through the font table at 51d6h.
The part exits only when the keyboard handler has observed Esc. Cleanup restores INT 09, restores the audio timer, unmasks the PIC, and returns to DOS.
Part .004: Tweaked Unchained VGA
.004 is the most VGA-register-heavy part. It briefly selects BIOS mode 12h,
then mode 13h, and immediately rewrites VGA registers into a planar/tweaked
layout:
21677 mov ax,0012h
2167a int 10h
2167c mov ax,0013h
2167f int 10h
21681 mov bx,0280h
21684 shr bx,3 ; CRTC offset = 0x50
21687 mov cx,0140h
2168a shr cx,2
2168d dec cx ; horizontal display end style value
2168e mov dx,03d4h
21691 mov al,11h
21693 mov ah,00h
21695 out dx,ax ; unlock CRTC protected registers
21699 mov al,01h
2169b mov ah,cl
2169d out dx,ax
216a1 mov al,13h
216a3 mov ah,bl
216a5 out dx,ax
It disables chain/odd-even behavior through Sequencer and CRTC registers:
216a6 mov dx,03c4h
216a9 mov al,04h
216ab out dx,al
216ad in al,dx+1
216ae and al,0f6h
216b0 out dx+1,al
216b1 mov dx,03d4h
216b4 mov al,14h
216b8 in al,dx+1
216b9 and al,0bfh
216bb out dx+1,al
216bc mov dx,03d4h
216bf mov al,17h
216c1 mov ah,0e3h
216c3 out dx,ax
This is the classic mode-X family trick: start from mode 13h, then unchain the
planes and make CRTC addressing explicit.
Planar Raw Page Upload
The routine at 21b55 loads a 160-line page from relative segment 0e0fh,
offset 0300h. It writes each byte four times through the map mask, one plane
at a time:
21b55 mov ds,0e0fh
21b5a mov si,0300h
21b62 xor di,di
21b65 mov cx,00a0h ; 160 rows
21b6c mov cx,0050h ; 80 address bytes per row
21b6f mov ax,0102h ; plane 0
21b72 out 03c4h,ax
21b73 lodsb
21b74 mov es:[di],al
21b77 mov ax,0202h ; plane 1
21b7a out 03c4h,ax
21b7b lodsb
21b7c mov es:[di],al
21b7f mov ax,0402h ; plane 2
21b82 out 03c4h,ax
21b83 lodsb
21b84 mov es:[di],al
21b87 mov ax,0802h ; plane 3
21b8a out 03c4h,ax
21b8b lodsb
21b8c mov es:[di],al
21b8f inc di
21b90 loop inner
21b92 add di,0050h ; skip to next displayed row in this layout
This is slower than a chunky rep movsd, but it is predictable: the source is
already arranged plane-by-plane for each destination address.
Column Strip Writer
21bc0 writes a 40-row column strip into four planes. The destination address
is based on cs:001a, and the same source pointer walks through four plane
chunks:
21bc0 mov bp,[cs:001a]
21bc5 mov dx,03ceh
21bc8 mov ax,4005h ; graphics controller write mode-ish setup
21bcc out dx,ax
21bcd mov dx,03c4h
21bd0 mov ax,0102h
21bd4 out dx,ax
21bd5 mov cx,0028h
21bd8 lodsb
21bd9 mov es:[bp],al
21bdd add bp,00a0h
; repeat for masks 2,4,8
0x00a0 is the row stride in this tweaked layout. The loop writes one vertical
strip in plane 0, backs up by 0x1900, then repeats for plane 1, plane 2, and
plane 3. This is used in the animation loop while the CRTC start address is
advanced.
Palette Raster Update
The most visually "copper-like" part of .004 is at 21832. It waits for
vertical retrace and then writes multiple DAC ranges:
21832 mov dx,03dah
21840 in al,dx
21841 test al,08h
21845 in al,dx
21846 test al,08h
2184a mov dx,03c8h
2184d mov al,1ah
2184f out dx,al
21851 rep outsb ; 0x12c bytes
21857 mov cx,002dh
2185c mov al,[phase_table + bx]
21861 out dx,al
21863 rep outsb
; repeated four times with bx += 5
The effect is not a hardware copper. It is CPU-timed DAC rewriting around vblank/retrace, with a small phase table selecting where the color bands start.
CRTC Start Scroll
Near the end of the frame loop it scrolls by bumping the CRTC start address:
21ad6 mov dx,03d4h
21ad9 mov ax,[cs:09ae]
21add xchg al,ah
21adf mov al,0dh
21ae1 out dx,ax
21ae2 mov ax,[cs:09ae]
21ae6 mov al,0ch
21ae8 out dx,ax
21ae9 inc word [cs:09ae]
21aee inc word [cs:001a]
That is a fine-grained hardware start-address scroll over the planar buffer.
The loop terminates on Esc from port 60h.
Part .006: Ending Picture And Text Strip
.006 returns to BIOS mode 0Dh and reuses the same 6-bit RLE planar decoder
family as .000.
Startup:
59a8 int 21h AH=4Ah
59af mov ax,000dh
59b2 int 10h
59b4 open TSH.DAT
59b7 read three config bytes
59ba close TSH.DAT
5a08 init shared audio engine
5a0b set identity EGA palette
The ending packed picture stream is at relative segment 00d0, offset 0080.
It is decoded twice:
first decode source 00d0:0080, groups 0x00c8, destination A000:0000
second decode source 00d0:0080, groups 0x00c8, destination A000:1f40
Because the source and parameters are identical, both pages contain the same mountain base picture. The duplication gives the part two page buffers for the later scroll/copy logic.
The decoder code at 5b6c is the same algorithm as .000: literal bytes are
written directly; bytes with top bits 11 repeat the next byte count = byte & 0x3f times; the Sequencer Map Mask advances through planes 1, 2, 4, and 8.
After the picture is decoded, the main frame loop does three jobs:
- Scroll/copy part of the back page into the front page during retrace.
- Draw a 20-word text strip from a table segment into the bottom area.
- Flip the visible CRTC start address between
0000hand1f40h.
The page copy is vblank-paced:
5abb mov dx,03dah
5ace add si,0028h
5ad1 mov cx,0a8ch
5ad4 in al,dx
5ad5 test al,08h
5ad9 in al,dx
5ada test al,08h
5ade rep movsb
5ae0 mov cx,0a8ch
; another retrace wait and copy
5aef mov cx,0a00h
; third retrace wait and copy
The text strip writer uses 20 independent source offsets:
5b05 mov bp,104bh
5b08 mov ds,[cs:0061]
5b0d mov di,[cs:1076]
5b12 add di,1f18h
5b16 mov cx,0014h
5b19 mov si,[cs:bp]
5b1d movsw ; one word per strip source
5b1e mov [cs:bp],si ; source pointer advances
5b22 add bp,0002h
5b25 loop 5b19
Then it flips pages:
5b27 mov bx,[cs:1076]
5b2c mov dx,03d4h
5b2f mov al,0dh
5b31 mov ah,bl
5b33 out dx,ax
5b34 mov al,0ch
5b36 mov ah,bh
5b38 out dx,ax
5b39 xor word [cs:1076],1f40h
The XOR toggles the visible base between the two 8000-byte pages.
Why The Demo Feels More Advanced Than Its Size
The fancy part is not one huge renderer. It is several small, tightly matched systems:
- a selector that uses DOS
EXECso each part can own memory and hardware; - packed child executables to keep archive size down;
- a byte-complete
RASPmodule/sample loader shared across later parts; - device-specific timer playback for LPT DAC, Sound Blaster, and PC speaker;
- planar RLE streams that are already ordered for VGA map-mask writes;
- page flips through CRTC
0Ch/0Dhinstead of copying full screens; - one part that uses standard mode
13hfor fast chunky sprite/strip work; - one part that starts from mode
13hand unchains/tweaks VGA for planar strip, palette, and start-address effects.
The code is early-1990s pragmatic. It does not try to build one clean engine.
It builds the minimum local kernel each visual part needs, then relies on the
common selector and RASP audio loader to hold the production together.
Limits Of This Pass
This pass does not reconstruct every font table or every text message, and it does not publish the historical contact details in the ending data. It also does not provide wall-clock timestamps from a captured AVI. The automatic intro timing is derived from retrace-counting loops; later parts are user-controlled and end on Esc.