Analysis model: gpt-5.5 xhigh
The Party 91 Intro by TRSI and Surprise! Productions - Technical Dissection
Release year: 1991
Party 91 is a small MS-DOS intro released at The Party 1991 by TRSI and
Surprise! Productions. The DIZ calls it an intro for TP91 and credits TRSI and Suprise! prod.; the runtime text credits Rick Dangerous and Burning Chrome
as the on-site makers, with a logo by J.O.E.
This is not a later polished multi-part DOS demo. It is more interesting as a late-1991 PC scene artifact: an MZ executable with a custom tail depacker, three external data files, direct VGA register work, a compact RLE bitplane decoder, palette stepping, CRTC scanline timing, and a simple text-page display engine.
The Party 1991 result file does not list a ranked PC intro compo. It lists the party as held in Aars, Denmark in late December 1991 and gives the main demo results for the larger party context. So I treat this as a The Party 1991 PC party intro, not as a ranked PC-compo placement.
Sources:
- Scene.org archive: https://archive.scene.org/pub/parties/1991/theparty91/in64/party91.zip
- Scene.org The Party 1991 results: https://archive.scene.org/pub/parties/1991/theparty91/info/results.txt
- Demozoo production page: https://demozoo.org/productions/2351/
- Demozoo party page: https://demozoo.org/parties/59/
Archive Contents
Unpacked archive:
FONT.P91 3,684 bytes
LOGO.P91 7,396 bytes
PARTY91.EXE 4,203 bytes
THANX.P91 17,689 bytes
FILE_ID.DIZ 63 bytes
Hashes used in this pass:
46673a5d84a20deb7bce6f9c2be15e53568d804ec12045863a843b9115bba293 party91.zip
2f32898e6a2516c2ee7800b1d9063569de0ea84beb0095530ca3d1bf7d6cd432 results.txt
274ff0b481ac0d67f1cd8f70f3930e32c1fe0385aa9262372ead030ec6c8197f FILE_ID.DIZ
0a6bdfc2f1b78a59ade2ee2a0fe58c0741184a6d49286193f48b84b74120ea1b FONT.P91
1adfe30a923b06449e923ff2838681f18b93d5f1e2d31db5bba93fb645505906 LOGO.P91
8a856c48c10bfde682f2ef080acda248256f70035eb432553f1fef270fafa280 PARTY91.EXE
87ed38d330848bc3e37ea86b682d37fa3861af48a86e4e836f3769aab90f580b THANX.P91
PARTY91.EXE is a normal MZ executable according to file type inspection, but
the load image is only the compressed wrapper plus a custom decompressor. The
visible data files are:
LOGO.P91: compressed planar logo data.FONT.P91: compressed 8x8 glyph sheet.THANX.P91: compressed full-screen/thanks page image data.
The runtime scroller begins with a welcome line for Surprise!/TRSI and says the party release was made by Rick Dangerous and Burning Chrome in the night between December 27 and December 28, 1991. I am only quoting short identifying text here; the rest is just party context and greetings.
MZ Wrapper And Packed Entry
The executable has a 32-byte MZ header:
header paragraphs: 0x0002
relocations: 0
file size: 4,203 bytes
load image size: 4,171 bytes
initial CS:IP: 00ef:000e
initial SS:SP: 053f:0080
min alloc: 0x046a paragraphs
max alloc: 0xffff paragraphs
The initial CS:IP maps to load-image offset 0x0efe, file offset 0x0f1e.
The entry is not the intro proper. It is a short stub that copies the packed
stream upward, then far-returns into relocated decompressor code:
0efe push es
0eff push cs
0f00 pop ds
0f01 mov cx,[000c]
0f05 mov si,cx
0f07 dec si
0f08 mov di,si
0f0a mov bx,ds
0f0c add bx,[000a]
0f10 mov es,bx
0f12 std
0f13 rep movsb
0f15 push bx
0f16 mov ax,002b
0f19 push ax
0f1a lret
The useful detail is the direction flag. The stub copies backward from the end of the packed stream so the decompressor can move itself and its input into a higher paragraph area without trampling bytes it still needs to read. The far return then changes both segment and offset in one cheap real-mode operation.
The following decompressor is a bit-coded LZ variant:
BPis the bit bucket.DLis the remaining bit count.- Plain bytes are copied with
movsb. - Backreferences copy from
ES:[DI+BX]toES:DI. - A match length code of zero exits the stream.
- A special length code of one updates the segment base when the output area crosses a paragraph boundary large enough to require rebasing.
- The final stub restores the stack and far-jumps through a pointer saved near the start of the unpacked image.
The packed entry is small, but it explains why the file is not directly disassemblable as the final intro. The final runtime is reconstructed from a DOSBox-X save after the unpacked code has reached its own setup path.
Runtime Capture Map
I used a DOSBox-X save state after PARTY91.EXE had unpacked and started.
In that save, the program name is PARTY91, the emulated machine is VGA, and
the PSP begins at physical 0x81c8. The carved runtime image used below starts
at physical 0x82c8; all offsets in this section are relative to that carved
image.
High-level runtime map:
0000 small palette/helper area
0020 attribute-register / tiny palette helper
0900 decoded font/glyph target area
0914 CRTC wave phase and text counters
1e60 character-to-glyph offset table
1f64 palette table A
2024 palette table B
20e4 palette table C
21a4 transition palette buffer
24a4 transition palette buffer
27a4 transition palette buffer
2864 transition palette buffer
2b64 text page count and page index
2b6a 40x6 text page stream
47c6 DOS memory-block shrink helper
47da DOS paragraph allocation helper
4860 DOS free-segment helper
486b ASCIIZ file loader
4933 RLE bitplane-to-chunky image decoder
49d3 retrace-synced DAC upload helper
49f0 palette interpolation helper
4a0b CRTC scanline split/waver
4a9b palette pulse/fade helper
4d74 8x8 glyph writer
4e10 file names and segment slots
4e34 main runtime entry after unpack
4e80 mode 13h and CRTC setup
4ee6 logo decode
4f0c font decode
4f34 palette fade toward table B
4f59 palette fade toward table C
4f93 main text/display loop
5094 keypress exit and closing transitions
The code/data layout is compact and old-school: there is no separate engine layer. Segment slots, file names, palettes, text pages, state words, and code all sit in one real-mode code segment, with external data loaded into allocated paragraph blocks.
DOS Memory And File Loading
The memory shrink routine at 47c6 resizes the original DOS block so the
program can allocate clean work areas after itself:
47c6 mov bx,ss
47c8 mov ax,es
47ca sub bx,ax
47cc mov ax,sp
47ce mov cl,4
47d0 shr ax,cl
47d2 add bx,ax
47d4 inc bx
47d5 mov ah,4ah
47d7 int 21h
47d9 ret
The calculation is the usual real-mode executable shrink:
paragraphs from PSP/owner block to SS
+ SP rounded down to paragraphs
+ one extra paragraph
= new DOS block size
The allocator at 47da wraps DOS int 21h / AH=48h. On success it stores the
returned segment through CS:[DI]. On failure it prints a short "not enough
memory" style message and exits.
47da mov ah,48h
47dc int 21h
47de jae 47ef
; print failure text and terminate
47ef mov cs:[di],ax
47f2 ret
The file loader at 486b is similarly direct. Its inputs are:
DX = pointer to ASCIIZ filename in CS
DI = pointer to the segment slot where the allocation result is stored
It performs:
open file with int 21h / AH=3Dh, AL=92h
seek to end with int 21h / AH=42h, AL=2
use AX as byte length
seek back to start
allocate (length >> 4) + 1 paragraphs
read length bytes with int 21h / AH=3Fh into the allocated segment
close file with int 21h / AH=3Eh
store allocated segment in CS:[DI]
The open failure path mutates the terminating zero byte of the filename into a
DOS $ terminator, prints a small complaint naming the missing file, and exits.
That is a very 1991 way to avoid keeping duplicate filename strings around.
External Asset Decode Calls
The main entry at 4e34 loads the three asset files and allocates one extra
screen-sized block:
4e5c mov di,4e19h
4e5f mov dx,4e10h
4e62 call 486b ; logo.p91
4e65 mov di,4e24h
4e68 mov dx,4e1bh
4e6b call 486b ; font.p91
4e6e mov di,4e30h
4e71 mov dx,4e26h
4e74 call 486b ; thanx.p91
4e77 mov di,4e32h
4e7a mov bx,0fa5h
4e7d call 47da ; offscreen full page block
The sizes match the later decoder calls:
- logo: 56 rows, 320 pixels wide, six bitplanes, destination in VGA memory.
- thanks page: 200 rows, 320 pixels wide, eight bitplanes, destination in the allocated page block.
- font: 17 rows of a wider glyph atlas, eight bitplanes, destination at
CS:0920.
The single image decoder at 4933 is parameterized enough to handle all three.
That reuse is the tightest part of the intro.
RLE Bitplane-To-Chunky Decoder
The decoder at 4933 converts a compressed bitplane stream into 8-bit chunky
pixels. It is not just an RLE expander; it expands bitplanes by setting or
clearing one bit in each destination byte.
Inputs:
DS:SI = compressed source stream
ES:DI = destination
AL = starting destination bit index
CX = row count
BP = destination row width in pixels
BX = extra row advance in pixels
DX = number of bitplanes to apply
The setup converts pixel widths into byte counts and builds the first bit mask:
4933 push ax
4934 mov ax,bp
4936 and ax,0007h
4939 shr bp,3
493c cmp ax,0
493f je 4944
4941 mov ax,1
4944 add bp,ax ; ceil(width / 8)
4946 shr bx,3 ; extra x advance, pixels to bytes
4949 pop ax
494a push cx
494b mov cl,al
494d mov al,1
494f shl al,cl ; AL = bit to set for this plane
4951 mov ah,al
4953 not ah ; AH = mask to clear that bit
Then it enters a three-level loop:
for every output row:
save row counters
for every bitplane requested by DX:
decode enough RLE packets to fill the row byte count
advance the destination bit mask to the next plane
move DI to the next destination row
The RLE packet byte at DS:SI decides between a literal run and a repeat run:
495d xor cx,cx
495f mov cl,[si]
4961 inc si
4962 cmp cl,7fh
4965 ja 4989
Low packet bytes are literals. The count is packet + 1; each following source
byte supplies eight pixels for the current destination bitplane:
4967 inc cx ; literal byte count = packet + 1
4968 sub bp,cx
496a push bx
496b mov bl,[si]
496d inc si
496e push cx
496f mov cx,8
4972 rcl bl,1
4974 jb 497c
4976 and es:[di],ah ; source bit 0: clear this plane bit
4979 jmp 497f
497c or es:[di],al ; source bit 1: set this plane bit
497f inc di
4980 loop 4972
4982 pop cx
4983 loop 496b
4985 pop bx
4986 jmp 49b1
The core is the rcl bl,1 plus and/or es:[di]. One compressed byte turns into
eight chunky pixels, but only touches a single bitplane in each pixel byte.
After all planes have run, those bits combine into normal 8-bit VGA color
indices.
High packet bytes are repeats. The count is 0x101 - packet; one source byte is
replayed that many times:
4989 sub cx,0101h
498d neg cx ; repeat byte count = 0x101 - packet
498f sub bp,cx
4991 push bx
4992 mov bl,[si]
4994 mov bh,bl
4996 inc si
4997 push cx
4998 mov cx,8
499b rcl bl,1
499d jb 49a5
499f and es:[di],ah
49a2 jmp 49a8
49a5 or es:[di],al
49a8 inc di
49a9 loop 499b
49ab mov bl,bh
49ad pop cx
49ae loop 4997
49b0 pop bx
The repeat path keeps a copy of the repeated source byte in BH, restores it
into BL for each repeated output byte, and runs the same eight-pixel bit test.
The row/plane tail restores the destination pointer and advances the bit masks:
49b1 cmp bp,0
49b4 jne 495d ; more packets needed for this row/plane
49b6 pop bp
49b7 pop di
49b8 pop cx
49b9 shl al,1
49bb stc
49bc rcl ah,1
49be loop 495a ; next bitplane
AL moves from bit 0 upward. AH is kept as the inverse clear mask, so it has
to rotate in a set bit as the clear position shifts. That is why the code uses
stc before rcl ah,1.
After all planes for the row are decoded, the destination advances by the visible row stride and the extra byte advance:
49c0 push bp
49c1 push bx
49c2 shl bp,3
49c5 shl bx,3
49c8 add di,bp
49ca add di,bx
49cc pop bx
49cd pop bp
49ce pop ax
49cf pop cx
49d0 loop 4956 ; next row
49d2 ret
Because BP and BX were byte counts after setup, the shifts rebuild pixel
units before adding to DI. For a 320-pixel row, this lands at the next VGA
scanline as expected.
Mode 13h And CRTC Setup
The display setup starts in BIOS mode 13h, then edits CRTC registers directly:
4e80 mov ax,0013h
4e83 int 10h
4e85 mov dx,03d4h
4e88 mov ah,0c8h
4e8a mov al,18h
4e8c out dx,ax
4e8d mov al,07h
4e8f out dx,al
4e90 inc dx
4e91 in al,dx
4e92 and al,0efh
4e94 out dx,al
4e96 mov al,09h
4e98 out dx,al
4e99 inc dx
4e9a in al,dx
4e9b and al,0bfh
4e9d out dx,al
The register writes are not a full custom mode set; they are tweaks on top of
mode 13h. Register 18h is the line compare register. Registers 07h and
09h contain overflow/double-scan-related bits, so clearing those bits helps
prepare the later split/wave behavior.
The code also sets the CRTC display start offset to 0x1f40:
4e9e mov bx,1f40h
4ea1 mov dx,03d4h
4ea4 mov ah,bh
4ea6 mov al,0ch
4ea8 out dx,ax
4ea9 inc al
4eab mov ah,bl
4ead out dx,ax
Then it unlocks write protection by clearing bit 7 of CRTC register 11h:
4eae mov dx,03d4h
4eb1 mov al,11h
4eb3 out dx,al
4eb4 inc dx
4eb5 in al,dx
4eb6 and al,7fh
4eb8 out dx,al
The intro is therefore still a 320x200 256-color program in spirit, but it uses CRTC start, line compare, and per-scanline register changes to create motion that is cheaper than redrawing the whole frame.
Initial Draw Sequence
After the VGA setup, the code uploads an initial 256-entry palette from 1f64:
4eb9 push cs
4eba pop ds
4ebb mov si,1f64h
4ebe mov di,0
4ec1 mov cx,0100h
4ec4 call 49d3
The logo is decoded straight into VGA memory. The destination DI=7bc0h is an
offset inside segment A000h; the call requests 56 rows, 320 pixels per row,
and six bitplanes:
4ecc push cs:[4e19h] ; LOGO.P91 segment
4ed1 pop ds
4ed2 mov si,0
4ed5 mov di,7bc0h
4ed8 mov cx,38h
4edb mov al,0
4edd mov bp,0140h
4ee0 mov bx,0
4ee3 mov dx,6
4ee6 call 4933
THANX.P91 is decoded into the separately allocated screen-sized page:
4ee9 push cs:[4e30h] ; THANX.P91 segment
4eee pop ds
4eef mov si,0
4ef2 mov es,cs:[4e32h]
4ef7 mov di,0
4efa mov cx,00c8h
4efd mov al,0
4eff mov bp,0140h
4f02 mov bx,0
4f05 mov dx,8
4f08 call 4933
FONT.P91 is decoded into the code segment at 0920:
4f0c push cs:[4e24h] ; FONT.P91 segment
4f11 pop ds
4f12 mov si,0
4f15 push cs
4f16 pop es
4f17 mov di,0920h
4f1a mov cx,11h
4f1d mov al,0
4f1f mov bp,0140h
4f22 mov bx,0
4f25 mov dx,8
4f28 call 4933
The FONT.P91 parameters look odd at first because the font only needs 8x8
glyphs, but the destination stride is still 320 bytes. The glyph writer later
uses the atlas as if each glyph row is laid out in a 320-byte-wide sheet.
DAC Upload And Palette Interpolation
Palette upload at 49d3 waits for a full retrace edge, sets the starting DAC
index, and streams CX * 3 bytes:
49d3 mov dx,03dah
49d6 in al,dx
49d7 test al,08h
49d9 jne 49d6
49db in al,dx
49dc test al,08h
49de je 49db
49e0 mov dx,03c8h
49e3 mov ax,di
49e5 out dx,al
49e6 inc dx
49e7 mov ax,cx
49e9 shl cx,1
49eb add cx,ax
49ed rep outsb
49ef ret
This is a classic clean-DAC write: wait until outside retrace, then wait until inside retrace, then stream RGB bytes while the beam is in the safer interval.
The interpolation helper at 49f0 nudges one palette toward another by a
fraction:
49f0 mov ax,cx
49f2 shl cx,1
49f4 add cx,ax ; CX = entries * 3
49f6 mov al,[di] ; target component
49f8 cbw
49f9 mov dl,[si] ; current component
49fb mov dh,0
49fd sub ax,dx
49ff idiv bl
4a01 add dx,ax
4a03 mov [si],dl
4a05 inc si
4a06 inc di
4a07 loop 49f6
4a09 ret
In plain terms:
delta = target - current
current += delta / divisor
The main code uses it in two nested fade stages:
4f34 mov cx,0010h
4f37 push cx
4f38 mov cx,0040h
4f3b mov si,1f64h
4f3e mov di,2024h
4f41 mov bl,10h
4f43 call 49f0
4f46 mov si,1f64h
4f49 mov di,0
4f4c mov cx,0040h
4f4f call 49d3
4f52 pop cx
4f53 loop 4f37
4f59 mov cx,0040h
4f5c push cx
4f5d mov cx,0040h
4f60 mov si,2024h
4f63 mov di,20e4h
4f66 mov bl,40h
4f68 call 49f0
4f6b mov si,2024h
4f6e mov di,0
4f71 mov cx,0040h
4f74 call 49d3
4f77 pop cx
4f78 loop 4f5c
Only 64 entries are faded in these stages. Later code manually pokes entries
0xfd and 0xfe for pulse colors used by the scanline/text effect.
CRTC Scanline Waver
The routine at 4a0b is the most hardware-specific part. It waits for vertical
retrace, writes a baseline CRTC offset value, then performs a 200-iteration
scanline loop. Inside that loop it waits on Input Status 1 bit 0, then writes
CRTC register 04h using a rolling table indexed by phase.
Entry:
4a0b mov bp,0001h
4a0e mov dx,03dah
4a11 in al,dx
4a12 test al,08h
4a14 je 4a11
4a16 in al,dx
4a17 test al,08h
4a19 jne 4a16
4a1b mov dx,03d4h
4a1e mov ax,0013h
4a21 out dx,ax ; CRTC index 13h, value 0
4a22 mov bx,004ah
4a25 add bx,cs:[0914h]
4a2a mov cx,00c8h
Per-scanline body:
4a2d mov dx,03dah
4a30 in al,dx
4a31 test al,01h
4a33 je 4a30
4a35 mov dx,03d4h
4a38 mov al,04h
4a3a out dx,al
4a3b inc dx
4a3c mov al,54h
4a3e mov ah,cs:[bx]
4a41 add al,ah
4a43 out dx,al
The index base is 0x4a + phase, and phase is stored at CS:[0914]. Each
scanline writes:
CRTC[04h] = 0x54 + table[0x4a + phase + local_step]
The code then advances through the table and occasionally writes CRTC register
13h from DS:SI:
4a4d inc bx
4a4e inc bx
4a4f add si,bp
4a51 cmp si,07d0h
4a55 jb 4a64
4a57 mov dx,03d4h
4a5a mov al,13h
4a5c out dx,al
4a5d inc dx
4a5e lodsb
4a5f out dx,al
4a60 neg bp
4a62 xor si,si
CRTC register 13h is the logical line offset. Changing it mid-frame changes
how the next scanlines step through VGA memory. Combined with changes to
register 04h, this gives the display a raster-split/wobble feel while the CPU
does very little framebuffer work.
At the end, the phase advances by two and wraps at 0x07d0:
4a6c mov ax,cs:[0914h]
4a70 inc ax
4a71 inc ax
4a72 cmp ax,07d0h
4a75 jne 4a7a
4a77 mov ax,0
4a7a mov cs:[0914h],ax
4a7e ret
That slow phase drift is why the split shape moves even if the text page itself is not redrawn every frame.
Palette Pulse Helper
The helper at 4a9b is a long unrolled palette pulse routine. Its repeated
shape is:
wait for display-status timing
compute an intensity from a countdown
write DAC index 0 or 0xfe
write three RGB components
loop for a small fixed count
One representative block:
4b44 mov cx,0010h
4b47 mov ah,cl
4b49 dec ah
4b4b shl ah,1
4b4d shl ah,1
4b4f add ah,14h
4b52 mov dx,03c8h
4b55 mov al,0feh
4b57 out dx,al
4b58 inc dx
4b59 mov al,02h
4b5b out dx,al
4b5c mov al,ah
4b5e out dx,al
4b5f mov al,02h
4b61 out dx,al
...
4b75 loop 4b47
The intro uses palette animation as a cheap brightness effect. Instead of changing many pixels, it changes a few DAC entries that the already-rendered pixels reference.
Glyph Writer
The glyph writer at 4d74 draws an 8x8 character to VGA memory. Inputs:
AL = character code
CX = x position in pixels
DX = y position in pixels
It first maps the character code to a glyph offset through the table at 1e60:
4d74 mov di,0a000h
4d77 mov es,di
4d79 cmp al,20h
4d7b jne 4d7f
4d7d mov al,61h
4d7f sub al,28h
4d81 mov bx,1e60h
4d84 xor ah,ah
4d86 shl ax,1
4d88 add bx,ax
4d8a mov si,cs:[bx]
4d8d add si,0920h
Space is special-cased before the 0x28 base subtraction. The font table
appears to pack only the characters needed by the intro text; the remap keeps
space inside that private table.
Destination offset is y * 320 + x, implemented with shifts:
4d91 shl dx,1
4d93 shl dx,1
4d95 shl dx,1
4d97 shl dx,1
4d99 shl dx,1
4d9b shl dx,1 ; DX = y * 64
4d9d mov di,dx
4d9f shl dx,1
4da1 shl dx,1 ; DX = y * 256
4da3 add di,dx ; DI = y * 320
4da5 add di,cx ; DI = y * 320 + x
The actual copy is unrolled: copy eight bytes, then add 0x138 to source and
destination. Since 8 + 0x138 = 0x140, each row advances one 320-byte stride.
4da7 mov cx,8
4daa rep movsb
4dac add di,0138h
4db0 add si,0138h
4db4 mov cx,8
4db7 rep movsb
4db9 add di,0138h
4dbd add si,0138h
; repeated until eight glyph rows have been copied
This is a fast enough character blitter for the intro because it writes only one glyph cell at a time. There is no need to redraw a complete text plane on each frame.
Text Page State Machine
The text system is a 40-column by 6-row page renderer. The data begins at
2b6a; the state words just before it are:
0914 CRTC wave phase
0916 signed step for the split/text pointer
0918 bounce index
091a blank/hold countdown
091c current column, 0..39
091e current row, 0..5
2b64 page count, value 5
2b66 current page
2b68 hold delay
2b6a text pages, 40 * 6 * 5 bytes
The main display loop starts at 4f93. It updates the pointer used by the
CRTC routine, calls the scanline waver, calls the palette pulse, and only then
decides whether to draw the next glyph:
4f93 push cs
4f94 pop ds
4f95 xor si,si
4f97 add si,cs:[0918h]
4f9c add si,cs:[0916h]
4fa1 mov cs:[0918h],si
4fa6 cmp si,0
4fa9 jg 4fb4
4fab neg word cs:[0916h]
4fb0 jmp 4fc3
4fb4 cmp si,07d0h
4fb8 jl 4fc3
4fba neg word cs:[0916h]
4fc3 mov di,si
4fc5 cmp word cs:[091ah],0
4fcb je 4fd2
4fcd xor si,si
4fcf dec word cs:[091ah]
4fd2 call 4a0b
4fd5 call 4a9b
0918 bounces between 0 and 0x07d0, with 0916 changing sign at the ends.
That value feeds the split routine. 091a can force a temporary blank pointer
while counting down.
If the hold delay at 2b68 is nonzero, the loop skips text drawing:
4fd8 cmp word cs:[2b68h],0
4fde je 4fe1
4fe0 dec word cs:[2b68h]
; skip the new-glyph path
When it does draw, it computes:
text_offset = 2b6a + row * 40 + column + page * 240
x = column * 8
y = row * 12 + 16
The row multiply by 40 uses shifts:
4fe1 mov ax,cs:[091eh] ; row
4fe5 shl ax,1 ; *2
4fe7 shl ax,1 ; *4
4fe9 shl ax,1 ; *8
4feb mov bx,ax
4fed shl ax,1 ; *16
4fef shl ax,1 ; *32
4ff1 add bx,ax ; row * 40
4ff3 add bx,cs:[091ch] ; + column
4ff8 add bx,2b6ah ; + page stream base
The page multiply by 240 is also shift-only:
4ffd mov cx,cs:[2b66h] ; page
5001 shl cx,1 ; *2
5003 shl cx,1 ; *4
5005 shl cx,1 ; *8
5007 shl cx,1 ; *16
5009 mov dx,cx ; page * 16
500b shl cx,1 ; *32
500d shl cx,1 ; *64
500f shl cx,1 ; *128
5011 shl cx,1 ; *256
5013 sub cx,dx ; page * 240
5015 add bx,cx
The x/y coordinate math:
5017 mov ax,cs:[091ch] ; column
501b shl ax,1
501d shl ax,1
501f shl ax,1 ; x = column * 8
5021 mov cx,ax
5023 mov ax,cs:[091eh] ; row
5027 shl ax,1
5029 shl ax,1 ; row * 4
502b mov dx,ax
502d shl ax,1 ; row * 8
502f add dx,ax ; row * 12
5031 add dx,0010h ; y = row * 12 + 16
5034 mov al,cs:[bx]
5037 call 4d74
Then the cursor advances through columns, rows, and pages:
503a inc word cs:[091ch]
503f cmp word cs:[091ch],0028h
5045 jne 5089
5047 mov word cs:[091ch],0
504e inc word cs:[091eh]
5053 cmp word cs:[091eh],0006h
5059 jne 5089
505b mov word cs:[091eh],0
5062 mov word cs:[091ch],0
5069 mov word cs:[2b68h],012ch
5070 inc word cs:[2b66h]
5075 mov ax,cs:[2b64h]
5079 cmp word cs:[2b66h],ax
507e jne 5089
5080 mov word cs:[2b66h],0
So the text does not scroll continuously. It types/reveals one glyph at a time
across a 40x6 page, waits for 0x012c frames after a page, then advances to
the next page, wrapping after five pages.
Keyboard polling is DOS function AH=0Bh:
5089 mov ah,0bh
508b int 21h
508d or al,al
508f jne 5094
5091 jmp 4f96
No key: loop forever. Any key: enter the closing sequence.
Closing Sequence
On keypress, the intro consumes the key, clears the first 32,000 bytes of VGA
memory, performs several palette transitions, copies the predecoded thanks page
from the allocated block to A000:0000, then fades again:
5094 mov ah,08h
5096 int 21h
5098 mov ax,0a000h
509b mov es,ax
509d xor di,di
509f mov cx,7d00h
50a2 xor al,al
50a4 rep stosb
The full-page copy:
5108 mov ax,cs:[4e32h]
510c mov ds,ax
510e mov cx,0fa00h
5111 xor si,si
5113 mov di,si
5115 rep movsb
0xfa00 is 64,000 bytes, exactly one 320x200 mode 13h screen. That is why
THANX.P91 was decoded into a separate block during startup: the exit can show
the final image with one linear copy instead of doing a decompression pass under
the final fade.
Finally it returns to text mode and exits:
mov ax,0003h
int 10h
mov ax,4c00h
int 21h
What Each Part Does
The intro divides cleanly into these functional parts:
- MZ entry/depacker: moves the packed stream upward, expands the real runtime, and jumps to it.
- DOS setup: shrinks the original executable block, loads external
.P91assets, and allocates an offscreen page. - Image decoder: expands RLE-coded bitplanes into chunky VGA pixels, reusing the same loop for logo, font, and final page.
- VGA setup: starts from BIOS mode 13h and edits selected CRTC registers.
- Palette engine: uploads DAC tables during retrace and interpolates 64-entry ranges for fades.
- Scanline waver: waits on VGA status bits and changes CRTC registers during the frame for a raster split effect.
- Text engine: renders 8x8 glyphs one at a time into VGA memory from five 40x6 pages.
- Exit path: clears part of the screen, fades, copies the already-decoded final page, fades again, restores text mode, and terminates.
The core technical identity is not any single visual. It is the combination of old real-mode DOS file discipline with direct VGA timing and small, reused inner loops. For 1991 PC demo work, that is the point: it sits between simple text-mode intros and the more structured multi-effect engines that would become common in the 1992-1994 PC scene.