Analysis model: gpt-5.5 xhigh

Demo 3 by The Space Pigs - Technical Dissection

Demo 3 / spdemo3 is a 1992 MS-DOS demo by The Space Pigs. The Hornet 1992 demo index lists spdemo3.zip as Demo 3 by The Space Pigs, with Sound Blaster/DAC support.

Release year: 1992

This is smaller and older-looking than Vicky, but technically it is a useful Space Pigs bridge: an LZEXE 0.91 main executable, an old intro resurrected for the April 1992 Atari and PC party in Uppsala, a direct mode 13h VGA setup, a custom whole-screen RLE decoder, a hardware start-address scroller, a column-at-a-time 40-line glyph renderer, small overlay-strip motion, and a four-channel sample/music player that can target internal speaker, Sound Blaster, or a parallel-port DAC.

Reconstructed Demo 3 RLE screen

The image above is reconstructed directly from the demo's built-in RLE stream and final DAC palette. The DOSBox-X runtime capture reached graphics mode but aborted in the emulator before a useful live demo frame could be extracted. The front-page card therefore uses this reconstructed screen directly, without manufacturing a single-frame GIF.

Sources

Archive

The examined archive is spdemo3.zip from the Hornet mirror:

spdemo3.zip  151149 bytes

ZIP contents:

SPDEMO3.EXE   144529 bytes   1992-04-19 16:09
STARPRT2.EXE    6400 bytes   1992-09-13 20:37

SPDEMO3.EXE is the demo. STARPRT2.EXE is a separate small MZ program in the same archive. I found no filename reference to it from the restored main demo. It contains board-ad style text and another embedded MZ marker, so it looks like a bundled side part rather than the main Demo 3 payload.

Hashes:

faa2b33d1125137c261a5de21c5eb9147436f4f0bd4a8a3b792dde7ac1d64f15  spdemo3.zip
6af39306341d3914221a9f2e647577796d27d5dd762fe6465d576d1a97d82c2b  SPDEMO3.EXE packed
5db6564cd597b83c615bdf4cffa475273a2bf6d587618c6c76469d5c48d2a205  UNP-expanded SPDEMO3.EXE
07955e28e32375e96598e48cdc4c6767a6af95158e0ca3c8ec3d484b613564df  STARPRT2.EXE

Packing And MZ Layout

The shipped main executable is LZEXE 0.91-compressed:

SPDEMO3.EXE packed file size  144529 bytes
MZ header size                   32 bytes
relocations                       0
CS:IP                         232D:000E
entry file offset              232FEh
SS:SP                         4499:0080

UNP expands it into a normal relocated MZ:

expanded file size             280471 bytes
MZ header size                    224 bytes
relocations                        49
CS:IP                          3E54:0000
entry load offset              3E540h
entry file offset              3E620h
SS:SP                          0000:04EC

Offsets below use the expanded load-image address: file offset minus the 00E0h-byte MZ header.

STARPRT2.EXE is not LZEXE according to file, but it is not a normal straight-line C/Pascal program either:

STARPRT2.EXE size                 6400 bytes
MZ header size                      32 bytes
relocations                          1
CS:IP                            0000:0003
entry file offset                  0023h

The entry bytes are dense and data-like, and there is another MZ marker near the end. I treated it as a sidecar instead of mixing it into the main demo analysis.

Internal Text And Dating

The restored main executable contains the scroller text in plain ASCII. It says The Space Pigs are presenting a more than year-old intro, that Anyware decided to release it at the Atari and PC party in Uppsala in April 1992, and that the tune is Le Cendrier Rouge by Deelite. It also says Anyware had spent the last 18 months coding Digistudio 1.5.

That text fits the archive timestamp: the code is older than April 1992, but the release package is a 1992 Hornet entry.

The main executable also contains the module name:

dear_rob.MOD

Runtime Capture

I attempted a bounded DOSBox-X 2026.01.02 run with video capture and the internal-speaker command-line argument:

SPDEMO3.EXE /042

The parser accepts three hex digits after /; 0042h is the same device word used by the spacebar/internal-sound startup path.

DOSBox-X reached the mode switch and wrote short ZMBV AVI files, then aborted with a host-side double-free. The first AVI only yielded the DOS prompt and the capture command; the second yielded a blank frame. I did not use those frames as visual evidence. The PNG in this article is therefore a static reconstruction from the demo's own RLE screen stream and palette.

Visual/Asset-To-Code Concordance

The published visual is a decoder-backed still, not a captured playback frame. It is tied to the executable because the setup code expands the same built-in RLE stream into the visible mode-13h framebuffer before the scrolling loop starts.

Visible artifact:

images/spdemo3-space-pigs-rle-screen.png
source stream: DS=004Fh, DX=0730h
expanded load offset: 0C20h
decoder path: 3EAA0h
destination: A000:0000
output size: FA00h bytes, 320x200 mode-13h screen
palette: final 256-color DAC table at load offset 2EF0h

The startup-to-screen path is:

LZEXE 0.91 restore / UNP-expanded MZ
  -> entry at load offset 3E540h
  -> optional /xxx device-port parser or speaker prompt
  -> BIOS mode 13h
  -> CRTC line-compare/start-address setup
  -> 3EAA0h whole-screen RLE decode to A000h
  -> final DAC palette at 2EF0h
  -> palette fade-in
  -> frame loop with CRTC hardware scroll, glyph columns, strip buffers, overlay text
  -> four-channel timer player through speaker, Sound Blaster, or LPT/Covox backend

The still and decoder agree exactly on size and format. The decoder stops when DI reaches FA00h, which is 64,000 bytes: one full 320x200 chunky VGA frame. The stream grammar is the same two-byte literal/run form used elsewhere in Space Pigs code: unequal byte pairs are copied literally, equal pairs are followed by a count byte and expanded as a run. The reconstructed PNG uses the stream beginning at expanded load offset 0C20h, and colors it with the DAC table that the main loop later uploads from 2EF0h.

The still is only the base screen. The analysis sections after it explain what the live demo adds over that base:

Evidence boundary: the DOSBox-X capture did not yield a useful live frame, so this page does not claim runtime timing or a GIF. Any future animation should come from a stable DOSBox/video capture of the actual loop. The existing image should remain a direct RLE-and-palette reconstruction, not a cropped web-page or lightbox screenshot.

High-Level Entry

The restored entry at load offset 3E540h is compact:

3E540  call 3E9A5h        ; parse /xxx port argument or prompt
3E543  push 004Fh
3E546  pop ds             ; main data segment
3E547  push A000h
3E54A  pop es
3E54B  call 3E6BBh        ; mode 13h, CRTC, RLE screen, buffers
3E54E  mov al, FFh
3E550  out 21h, al        ; mask PIC while starting player
3E552  mov dx, [0720h]    ; selected output device/port
3E556  mov cx, 34BCh      ; player speed value
3E559  call 3EBF4h        ; start audio
3E55C  call 3E950h        ; palette fade-in
3E55F  mov al, FEh
3E561  out 21h, al        ; unmask timer IRQ only

The frame loop is then:

3E563  mov dx, 03DAh
3E566  in al, dx
3E567  test al, 08h
3E569  je 3E566h          ; wait for vertical retrace start
3E56B  in al, dx
3E56C  test al, 08h
3E56E  jne 3E56Bh         ; wait for retrace end

3E570  mov dx, 03C8h
3E573  xor al, al
3E575  out dx, al
3E576  inc dx
3E577  mov cx, 0300h
3E57A  mov si, 2EF0h
3E57D  lodsb
3E57E  out dx, al
3E57F  loop 3E57Dh        ; upload 256*RGB DAC table every frame

3E581  push 004Fh
3E584  pop ds
3E585  push A000h
3E588  pop es
3E589  call 3E5B2h        ; hardware-scroll text/glyph columns
3E58C  call 3E80Fh        ; small shift buffer update
3E58F  call 3E758h        ; strip/buffer motion
3E592  call 3E888h        ; moving debug-text overlay
3E595  in al, 60h
3E597  cmp al, 01h
3E599  jne 3E563h         ; Esc exits

The exit path stops the player with DX=0, unmasks the PIC, restores text mode 03h, and terminates via INT 21h AX=4C00h.

Command-Line Parser

3E9A5h scans the PSP command tail at 0081h for / followed by exactly three hex digits. It builds a device/port word in DX by shifting each nibble:

3E9A7  mov bl, 08h
3E9A9  xor dx, dx
3E9AC  mov cx, 0003h
3E9AF  mov si, 0081h
3E9B2  lodsb
3E9B3  cmp al, 0Dh
3E9B5  je 3E9F8h          ; no argument: prompt
3E9B7  cmp al, '/'
3E9B9  jne 3E9B2h
...
3E9E7  cbw
3E9E8  push cx
3E9E9  mov cl, bl
3E9EB  shl ax, cl         ; first digit << 8, second << 4, third << 0
3E9ED  pop cx
3E9EE  or dx, ax
3E9F0  sub bl, 04h
3E9F3  loop 3E9BBh

If the command-line parse fails, it prints the setup text at DS=004Fh, DX=0630h with INT 21h AH=40h, waits with INT 16h, and accepts:

Space  -> DX = 0042h, internal speaker
Enter  -> exit to DOS

The prompt text mentions 278, 378, 3BC, and 220 as useful values: parallel-port DAC addresses and Sound Blaster base 220h. The selected word is stored at DS:0720h.

Mode 13h Setup

3E6BBh sets the video mode and constructs the initial screen:

3E6BB  push ax
3E6BC  mov ah, 00h
3E6BE  mov al, 13h
3E6C0  int 10h            ; 320x200 chunky VGA
3E6C3  mov dx, 03C8h
3E6C6  xor al, al
3E6C8  out dx, al
3E6C9  inc dx
3E6CA  mov cx, 0300h
3E6CD  mov si, 0330h
3E6D0  lodsb
3E6D1  out dx, al
3E6D2  loop 3E6D0h        ; startup palette, initially black

It then calls 3E685h, which unlocks and tweaks CRTC state:

3E685  mov dx, 03D4h
3E688  mov al, 18h
3E68A  mov ah, 27h
3E68C  shl ah, 1
3E68E  out dx, ax         ; line compare low bits
...
3E6AD  mov al, 09h
3E6AF  out dx, al
3E6B1  in al, dx+1
3E6B3  and al, BFh
3E6B7  mov ah, al
3E6B9  out dx, ax         ; max scanline bit adjustment

The setup also clears CRTC protection, sets vertical-display related state, uses Graphics Controller register 06h, decodes the built-in screen image, and finally positions the displayed page with CRTC start address 2F80h.

Whole-Screen RLE Decoder

The screen art is decoded by 3EAA0h. The setup calls it with:

3E6FD  mov dx, 0730h      ; source offset in DS=004Fh
3E700  xor di, di         ; destination A000:0000
3E703  push cs
3E704  call 3EAA0h

The decoder uses A000h as destination, keeps its scratch state in segment 13D9h, but reads compressed bytes from the caller's original DS segment. With DS=004Fh, the RLE stream starts at expanded load offset:

004Fh * 16 + 0730h = 0C20h

The loop runs until DI reaches FA00h, i.e. 64,000 bytes:

3EABD  mov si, [0002h]    ; source offset, initially 0730h
3EAC1  mov di, [0000h]    ; destination offset

3EAC6  push ds
3EAC7  mov bx, [0004h]    ; caller source segment, 004Fh
3EACB  mov ds, bx
3EACD  lodsw              ; fetch two source bytes
3EACE  pop ds
3EACF  cmp al, ah
3EAD1  je 3EAE1h          ; equal pair means run packet

3EAD3  stosw              ; unequal pair: two literal bytes
3EAD4  mov ax, FA00h
3EAD7  add ax, [0000h]
3EADB  cmp di, ax
3EADD  jb 3EAC6h

Run packets are a neat two-byte marker plus one count byte:

3EAE1  xchg ax, cx        ; AL/AH held repeated value
3EAE3  push ds
3EAE7  mov ds, [0004h]
3EAE9  lodsb              ; count byte
3EAEA  pop ds
3EAEB  xchg ax, cx
3EAEC  xor ch, ch
3EAEE  rep stosb          ; repeat marker byte count times

So the compressed format is:

byte A, byte B where A != B  -> output A, B
byte A, byte A, byte N       -> output A repeated N times

The reconstructed PNG uses this exact rule, the stream at load 0C20h, and the final DAC table at load 2EF0h. The decoder consumed 27B1h bytes to fill the full 64,000-byte screen buffer.

Palette Fade

The final palette lives at load offset 2EF0h. The startup palette area at DS=004Fh:0330h is initially black. 3E950h fades the first 16 colors by scaling the final palette into that working area and uploading it over successive retraces:

3E952  mov cx, 00FCh
3E955  mov si, 2EF0h
3E958  mov di, 0330h
3E95B  lodsb
3E95C  xor ah, ah
3E95E  shl ax, 4
3E961  div cl             ; source * 16 / fade_divisor
3E963  stosb
3E964  cmp di, 0360h
3E968  jb 3E95Bh

After each scaled batch, it waits for retrace and writes 30h DAC bytes:

3E96A  mov dx, 03DAh
3E96D  in al, dx
3E96E  test al, 08h
3E970  je 3E96Ah
...
3E978  mov dx, 03C8h
3E97B  xor al, al
3E97D  out dx, al
3E97E  inc dx
3E97F  mov cx, 0030h
3E982  mov si, 0330h
3E985  lodsb
3E986  out dx, al
3E987  loop 3E985h

The divisor decreases by four until it reaches 10h. Once the main loop is running, the full 0300h-byte palette at 2EF0h is uploaded each frame.

Hardware Scroller And Glyph Columns

3E5B2h is the central visual loop. It combines CRTC start-address movement with just-in-time glyph-column drawing.

The state words in DS=004Fh are:

0000h  current CRTC start address
0002h  current glyph source offset
0004h  scroller text pointer
0006h  phase/countdown until next character column

The routine starts by skipping work while 00C6h is nonzero. Otherwise it advances the hardware scroll and decrements the character phase:

3E5BA  inc word [0000h]       ; move CRTC start address
3E5BE  sub word [0006h], 0004h
3E5C3  jl 3E5D1h              ; need a new character?
3E5C5  add word [0002h], 00A0h
3E5CB  mov si, [0002h]
3E5CF  jmp 3E62Ah

If a new character is needed, the routine fetches the next byte from the scroller text:

3E5D1  mov bx, [0004h]
3E5D5  inc word [0004h]
3E5D9  mov al, [bx]
3E5DB  cmp al, 02h
3E5DD  jne 3E5EBh
3E5DF  call 3EA27h            ; special end/transition marker
3E5E2  mov word [0000h], 2F80h

Marker 03h rewinds the text pointer to 33B0h. Space is special-cased to a blank glyph at 2E40h with phase 000Ch.

For normal characters, it uses XLAT through a table at 0008h, looks up the glyph width/count at 006Ah + index*2, and computes the glyph source as index * 05A0h:

3E606  mov bx, 0008h
3E609  xlat
3E60A  mov bl, al
3E60C  xor bh, bh
3E60E  shl bx, 1
3E610  add bx, 006Ah
3E614  mov cx, [bx]
3E616  inc cx
3E617  mov [0006h], cx

3E61F  mov ax, 05A0h
3E622  mul bx                 ; glyph source block
3E624  mov si, ax
3E626  mov [0002h], si

The actual inner loop draws four adjacent columns, each 40 pixels tall. The destination is based on the current CRTC start address:

3E62A  mov di, [0000h]
3E62E  dec di
3E62F  shl di, 1
3E631  shl di, 1              ; byte address = start * 4
3E633  add di, 013Ch          ; near the right edge
3E637  push 0405h
3E63A  pop ds                 ; glyph bank segment

One column pass:

3E63B  mov cx, 0028h          ; 40 rows
3E63E  movsb                  ; one pixel/byte for this row
3E63F  add di, 013Fh          ; 319, so total row step is 320
3E643  loop 3E63Eh

After a column, DI is adjusted back upward by almost exactly one 40-line block:

3E645  sub di, 31FFh

Since one column loop advances 40 * 320 = 3200h bytes, subtracting 31FFh lands the next pass one byte to the right. The routine repeats this four times, producing a 4-pixel-wide vertical slice. That is the main economy of the scroller: hardware start-address movement handles the smooth displacement, and the CPU only draws a narrow strip when the next glyph column is needed.

At the end it writes the CRTC start address:

3E673  mov bx, [0000h]
3E677  mov dx, 03D4h
3E67A  mov al, 0Ch
3E67C  mov ah, bh
3E67E  out dx, ax
3E67F  inc al
3E681  mov ah, bl
3E683  out dx, ax

End/Transition Scroll Loop

Marker 02h enters 3EA27h. This loop scrolls the start address in larger steps and fills a 40-line block with descending color bytes:

3EA27  sub word [0000h], 0008h
3EA2C  cmp word [0000h], 2F80h
3EA32  ja 3EA35h
3EA34  ret

3EA35  mov di, [0000h]
3EA39  push A000h
3EA3C  pop es
3EA3D  shl di, 2
3EA40  mov al, 5Fh
3EA42  mov cx, 0028h

Each row writes 32 bytes, skips the remainder of the 320-byte line, then decrements the fill color:

3EA45  push cx
3EA46  mov cx, 0020h
3EA49  rep stosb
3EA4B  add di, 0120h
3EA4F  dec al
3EA51  pop cx
3EA52  loop 3EA45h

Then it waits for retrace, writes the CRTC start address, uploads the final palette, runs the same strip/overlay update routines as the main loop, and checks Esc. This is a mini-loop inside the scroller rather than a one-shot transition.

Small Shift Buffer

3E80Fh runs every second call:

3E80F  inc byte [032Dh]
3E813  test byte [032Dh], 01h
3E818  je 3E81Bh
3E81A  ret

It shifts a 00EAh-byte buffer backward from 01B3h to 01B9h:

3E81B  push 004Fh
3E81E  pop es
3E81F  mov si, 01B3h
3E822  mov di, 01B9h
3E825  mov cx, 00EAh
3E828  std
3E829  rep movsb
3E82B  cld

It then copies three bytes from 3061h into 00CAh, mirrors them, and uses a small table at 0249h to replace the three bytes at 3010h. This is a rolling buffer update feeding the later strip-composition routine, not a full screen redraw.

Strip Composition

3E758h builds several small source strips under DS=004Fh. The beginning copies overlapping bytes backward from 3061h to 3070h, making a repeated or smeared strip:

3E758  push 004Fh
3E75B  pop es
3E75C  mov si, 3061h
3E75F  mov di, 3070h
3E762  mov cx, 0054h
3E765  movsw
3E766  movsb
3E767  sub si, 0006h
3E76A  loop 3E765h

It then copies fixed chunks into 30C1h, 30D0h, and nearby buffers:

3E76C  mov si, 3010h
3E76F  mov di, 30C1h
3E772  mov cx, 0018h
3E775  rep movsb

3E777  mov cx, 0120h
3E77A  mov si, 3070h
3E77D  mov di, 30D0h
3E780  rep movsb

Four counters at CS:06B0h through CS:06B3h control how much of a repeated blank/filler region is copied into 3070h, 30D0h, 3130h, and 3190h. Each active counter is halved, used as a byte count three times, and then decremented by two:

3E782  mov cl, cs:[06B0h]
3E789  shr cx, 1
3E78D  mov si, 01C9h
3E790  mov di, 3070h
3E793  mov dx, cx
3E795  rep movsb
3E797  mov cx, dx
3E799  rep movsb
3E79B  mov cx, dx
3E79D  rep movsb
3E79F  sub byte cs:[06B0h], 02h

The same idiom repeats for the other three destinations. This is a compact multi-strip wipe/fill mechanism.

Moving Debug-Text Overlay

3E888h starts by copying 00F0h bytes from 00CAh to 2F20h:

3E896  push 004Fh
3E899  pop es
3E89A  mov di, 2F20h
3E89D  mov si, 00CAh
3E8A0  mov cx, 00F0h
3E8A3  rep movsb

It then places six 21-byte fragments of the anti-debug text into positions selected by table 3A40h and index [01BAh]:

3E8A5  mov bx, [01BAh]
3E8A9  mov di, [bx+3A40h]
3E8AD  and di, 00FFh
3E8B1  add di, 2F20h
3E8B5  mov si, 02A9h
3E8B8  mov cx, 0015h
3E8BB  rep movsb

Then:

3E8BD  add bx, 000Ah
3E8C0  and bx, 00FFh

and the same placement is repeated for fragments at:

02A9h, 02BEh, 02D4h, 02EBh, 0300h, 0316h

At the end it advances the phase:

3E944  add word [01BAh], 0003h
3E949  and word [01BAh], 00FFh

The visible effect is a moving set of short text fragments inside the lower strip buffer. It is also a nice joke: the program contains a long repeated anti-debugging message, then uses pieces of it as visual material.

Player Dispatch

The audio/player entry is 3EBF4h. If DX=0, it runs the stop/restore path. Otherwise it stores the chosen timing value and initializes the playback backend:

3EBF4  push 004Fh
3EBF7  pop ds
3EBF8  cmp dx, 0000h
3EBFB  je 3EC48h
3EC00  mov [325Ch], cx
3EC04  push dx
3EC12  call 40551h        ; clear low IVT/vector scratch
3EC15  call 40322h
3EC18  call 40432h        ; build sample scaling table
3EC1B  call 4048Fh        ; sample pre-pass/checksum-ish pass
3EC1E  call 4045Bh
3EC21  jb 3EC2Ch
3EC26  call 40513h
3EC29  call 401C7h        ; install timer
3EC2C  pop dx
3EC2D  call 3EC34h        ; backend dispatch
3EC30  call 402CCh        ; PIT divisor from selected speed

The backend dispatch is:

3EC34  cmp dx, 0042h
3EC37  jne 3EC3Ch
3EC39  jmp 40429h         ; internal speaker

3EC3C  cmp dx, 0220h
3EC40  jne 3EC45h
3EC42  jmp 403FBh         ; Sound Blaster

3EC45  jmp 403E3h         ; LPT/Covox-style DAC output

This matches the startup prompt exactly: 042h is internal speaker, 220h is Sound Blaster, and parallel-port values fall through to the DAC path.

Timer Install And Restore

401C7h installs the timer interrupt. It stores the old vector 08h in CS:1C83h/CS:1C85h, points the IVT vector at this player, programs PIT channel 0, and marks the player active:

401C7  pusha
401C8  push ds
401C9  push es
401CA  push 004Fh
401CD  pop ds
401CE  push 0000h
401D0  pop es
401D1  cli
401D2  mov ax, 07E4h
401D5  xchg ax, es:[0020h]
401DA  mov cs:[1C83h], ax
401DE  mov ax, cs
401E0  xchg ax, es:[0022h]
401E5  mov cs:[1C85h], ax

401ED  mov al, 36h
401EF  out 43h, al
401F1  mov bx, 5D37h
401F6  out 40h, al        ; low divisor byte
401FA  out 40h, al        ; high divisor byte
401FC  mov word [325Eh], 0001h
40202  call 402AAh
40205  sti
40206  mov byte cs:[1CD1h], 01h

The stop path restores the vector, resets the PIT, clears port 61h speaker enable bit 0, writes one final byte to the selected output port, and restores PIC state:

40219  push 0000h
4021B  pop es
4021C  mov ax, cs:[1C83h]
40220  mov es:[0020h], ax
40224  mov ax, cs:[1C85h]
40228  mov es:[0022h], ax
4022C  mov al, 36h
4022E  out 43h, al
40230  xor bx, bx
40235  out 40h, al
40239  out 40h, al
4023B  in al, 61h
4023D  and al, FEh
4023F  out 61h, al
40241  mov dx, cs:[0875h]
40246  out dx, al

402CCh later reprograms PIT timing based on the selected speed:

402D8  cli
402D9  mov al, 36h
402DB  out 43h, al
402DD  mov dx, 0012h
402E0  mov ax, 34DCh
402E3  mov bx, [325Ch]
402E7  div bx             ; divisor from 0012:34DC / speed
402ED  out 40h, al
402F1  out 40h, al
402F3  mov ax, [325Ch]
402F8  mov bx, 0032h
402FB  div bx
402FD  mov [325Eh], ax
40300  sti

Four-Channel Interpreter

The timer body processes four similar channel state blocks. The main body is visible from 3ED34h onward. A representative channel fetch:

3ED34  lds bx, cs:[05C2h]     ; current sample/data pointer
3ED39  cmp bx, 4327h
3ED3D  jae wrap_channel_0
3ED3F  mov bl, [bx]           ; sample byte
3ED41  xor bh, bh
3ED43  mov bl, cs:[bx+2077h]  ; remap/amplitude table
3ED48  mov dx, bx             ; contribution to mixed output

The same structure repeats for channel state blocks at:

05C2h / 05FEh / 063Ah / 0676h

Each channel has a pointer, limit, step/frequency values, and a remap-table base. The event interpreter uses 4-byte pattern/event cells and handles ProTracker-like concepts: note/sample selection, frequency/period changes, slide directions, vibrato/tremolo-ish nibbles, and speed changes. The command decoder is duplicated for each channel, so the code is larger but very direct.

The mixed result is eventually written through backend-specific code. The plain DAC path stores the selected output port in CS:0875h; the speaker path programs PIT channel 2 and port 61h; the Sound Blaster path probes the DSP reset/read ports before patching the output path.

Sound Blaster And Speaker Helpers

The Sound Blaster probe starts at 4032Dh. It tests candidate base ports, resets the DSP, and waits for AAh from base+0Ah:

4032D  pushf
4032E  cli
4032F  mov bp, 021Ch
40332  mov dx, bp
40334  sub dx, 0006h
40337  mov al, 01h
40339  out dx, al
...
40349  mov dx, bp
4034B  sub dx, 0002h
4034E  in al, dx
4034F  cmp al, AAh
40351  je 40369h

On success it stores the DSP port and marks the SB path available:

40369  mov dx, bp
4036B  in al, dx
4036C  test al, 80h
4036E  jne 4036Bh
40370  mov al, D1h
40372  out dx, al
40373  mov cs:[1E48h], dx
40378  mov cs:[07CFh], dx
4037D  mov byte [3280h], 01h

The internal speaker setup at 4038Ah enables speaker gate bits, programs PIT channel 2, and returns DX=0042h:

40399  in al, 61h
4039F  or al, 03h
403A1  out 61h, al
403A3  mov al, A0h
403A5  out 43h, al
403A7  xor al, al
403A9  out 42h, al
403AB  mov al, 90h
403AD  out 43h, al
403B1  mov dx, 0042h

Overall Structure

Demo 3 is old code by the author's own scroller text, but it is not trivial. Its main design is:

Compared with Vicky and Xmas '91, this is less planar and more mode-13h chunky, but the habits are recognizably Space Pigs: compact asset streams, hardware CRTC movement, direct port writes, self-contained audio backends, and small precomputed tables used to avoid expensive per-frame work.