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.

Decoded Skyhawks Xmas '91 stills

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

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:

  1. Scroll/copy part of the back page into the front page during retrace.
  2. Draw a 20-word text strip from a table segment into the bottom area.
  3. Flip the visible CRTC start address between 0000h and 1f40h.

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:

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.