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.

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
- Hornet 1992 demo index: https://ftp.scene.org/mirrors/hornet/demos/1992/00_index.txt
- Scene.org archive: https://ftp.scene.org/mirrors/hornet/demos/1992/spdemo3.zip
- Scene.org file page: https://files.scene.org/view/mirrors/hornet/demos/1992/spdemo3.zip
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:
3E5B2hmoves the display by changing the CRTC start address and draws only four new glyph columns at the right edge.3EA27his the end/transition mini-loop that scrolls in larger steps and fills a descending-color block.3E80Fhand3E758hmaintain the rolling strip and smear buffers.3E888hturns anti-debug text fragments into a moving lower-strip overlay.3EBF4hand the timer/player routines run the four-channel music/sample system and are not part of the still image itself.
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:
- Restore an LZEXE 0.91-packed real-mode MZ.
- Parse a manual output-port argument or fall back to an internal-speaker prompt.
- Enter mode
13h, tweak CRTC state, and decode a full 64,000-byte RLE screen. - Fade and then repeatedly reload a DAC palette.
- Move the display with the CRTC start address.
- Draw only narrow 4-pixel glyph columns at the right edge of the scroller.
- Maintain small strip buffers for smear/wipe effects.
- Run a four-channel timer-driven sample/music player with selectable output backends.
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.