Analysis model: gpt-5.5 xhigh
Panic by Future Crew - Technical Dissection
Scope
This is a direct runtime and reverse-engineering pass over Panic by Future Crew. Demozoo lists it as the 2nd-place PC demo at The Party 1992, and the release text in the archive says the final public release was delayed until 4 February 1993 because the party version hung on machines slower than a 33 MHz 486.
Public references:
- Demozoo production: https://demozoo.org/productions/711/
- Pouet production: https://www.pouet.net/prod.php?which=479
- Scene.org archive entry: https://files.scene.org/view/parties/1992/theparty92/demo/panic.zip
- Direct archive URL used here: https://archive.scene.org/pub/parties/1992/theparty92/demo/panic.zip
The most useful primary text source is PANIC.NFO inside panic.zip. It
credits Wildfire for main coding, PSI for the music player and fractal
routines, Trug for the greetings scroller, Purple Motion for the 16-channel
techno track, Pixel for logos and bitmap graphics, and Abyss for 3D object
design. It also says the demo wants a fast 386/486, about 600 KB of
conventional memory, a fast VGA card, and EMS for music.
The runtime pass follows the complete show. The static pass then explains what the executable actually does: the container, embedded packed MZ programs, Mode X register programming, palette loops, projection loops, plane blitters, text kernels, and run-length renderers. The visual names for some stages are inferred from code shape and file strings, not from source-level labels.
Complete Direct Silent Run
I ran the unmodified PANIC.EXE from the examined archive in DOSBox-X
2026.01.02, selected its own highlighted Execute the demo entry, and let it
return to DOS at about 4:08. The machine profile was S3 VGA, 16 MB RAM, EMS,
and a fixed 50,000-cycle CPU. Sound Blaster emulation remained active inside
DOSBox because the player drives the show's timing, but SDL sent host audio to
a dummy device. The lossless FFV1 working capture therefore contains one
video stream and no audio stream; the page keeps only direct silent GIFs.
A useful failure test clarified that dependency. With emulated sound hardware disabled, the volcano continued animating but the stage chain stopped advancing. Restoring internal Sound Blaster timing made the same executable complete normally. That proves a player/timing dependency in this run, not a specific failed instruction inside PSI's player.
The opening builds the logo through tile-sized pieces rather than presenting one finished bitmap. The changing division between the cool metallic letters and hot interior is the effect:

The black-and-white tunnel is deliberately harsh. Its rings reverse figure and ground while yellow line drawings are written over the centre:

The next part frames a small, dark-blue moving texture. Most of the screen is static ornament, making the bright diagonal movement inside the aperture read more strongly:

Then the aperture is overwhelmed by the red height field. The sampled surface pushes toward the viewer until it escapes the frame:

The wireframe sequence changes scale and topology continuously rather than merely spinning one mesh. Sparse blue points and connected edges alternately read as solid volumes and collapsing constructions:

Its restrained Future Crew stone logo is a pause between the geometric stage and the full-screen colour field:

The colour field is all motion: broad red, white, and cyan bands bend and slide while the stone logo holds a stable foreground reference:

Finally, the outro changes visual language again. Dense particles, vertical lettering, and a face emerge from noise before the executable returns cleanly to DOS:

Examined Archive
The archive used here is panic.zip from the The Party 1992 demo directory.
0399c0e923691621e81c99f1a70a8339b4288652290682bef2c191482fb3e75b panic.zip
Files:
| File | Size | SHA-256 |
|---|---|---|
PANIC.EXE |
1,693,874 | a7ceeae290a7132880cd6ba616c2b0bb4001b2f7c83694f04052314fa26df6e7 |
PANIC.NFO |
21,248 | 85907b4cf03db433ea8693aea9fee11284bd6c30a6a12a7defa8ce99b75407e0 |
GENERAL.CDN |
5,028 | 255328403c6dec7b2d62556ece467e015655900e8d310f0abeb5888bd28cc759 |
VOTING.FRM |
3,158 | c13cdda159119d3248cbf173d700cbac6b0a3a68c99840776d52866eda9c1e8c |
FILE_ID.DIZ |
495 | e38c40b656a6d7e59b7c409c10d1afd0bd800b478adc05c696e6afa16fe5ef8f |
FILE_ID.DIZ identifies the public release date as 4 February 1993. The
archive is a good target for analysis because PANIC.EXE is not a single
monolithic packed program. It is a loader followed by several independently
packed 16-bit executable stages.
Top-Level Container
PANIC.EXE starts as a normal MZ executable, but the MZ image described by the
header is only the first 28,561 bytes. The remaining 1.66 MB is overlay data,
including eight embedded PKLITE-packed MZ programs.
MZ header size: 32 bytes
MZ image size: 28,561 bytes
file size: 1,693,874 bytes
overlay size: 1,665,313 bytes
relocations: 0
entry: 06ae:0006
entry file offset: 0x6b06
The first payload has an SGI marker at file offset 0x20. This top program
is not one of the visual parts. It is the wrapper/loader that arranges the
environment and transfers to embedded programs. It contains low-level probes
and setup code, including BIOS interrupt use, smsw-style CPU state checks,
and self-modifying control flow. That code matters for execution, but the
visible demo is mostly in the embedded PKLITE programs.
The embedded MZ boundaries are clean:
| Item | File offset | Packed bytes | End offset | Notes |
|---|---|---|---|---|
| wrapper | 0x000000 |
0x006f91 |
0x006f91 |
top SGI wrapper MZ |
| embedded stage 1 | 0x006f92 |
0x0014c5 |
0x008457 |
small PKLITE-packed MZ |
| embedded stage 2 | 0x09e3f6 |
0x004de7 |
0x0a31dd |
PKLITE-packed MZ |
| embedded stage 3 | 0x0b5546 |
0x002da7 |
0x0b82ed |
PKLITE-packed MZ |
| embedded stage 4 | 0x0d82ed |
0x005854 |
0x0ddb41 |
PKLITE-packed MZ |
| embedded stage 5 | 0x0f8f1e |
0x004b9a |
0x0fdab8 |
PKLITE-packed MZ |
| embedded stage 6 | 0x10061c |
0x006249 |
0x106865 |
PKLITE-packed MZ |
| embedded stage 7 | 0x1834d5 |
0x011091 |
0x194566 |
PKLITE-packed MZ |
| embedded stage 8 | 0x196d43 |
0x004aff |
0x19b842 |
PKLITE-packed MZ |
The embedded program offsets are very far apart because PANIC.EXE also
contains bitmap, palette, music, object, and scroller data between the packed
programs.
Unpacking
The embedded stages carry PKLITE signatures. Generic raw depackers can recover
code bytes for many of them, but DOS-side PKLITE -x repairs the MZ headers
and relocation tables more cleanly. Running the embedded stage files through
PKLITE under DOSBox-X produced usable unpacked EXEs for embedded stages 2
through 8. Embedded stage 1 remained a small packed/runtime-looking program in
this pass, so the detailed disassembly below starts with STAGE02.EXE.
Recovered executable headers:
| Stage | Repaired file | Size | Header | Relocs | Entry | Stack |
|---|---|---|---|---|---|---|
| 2 | STAGE02.EXE |
44,176 | 0x0200 |
105 | 0000:0000 |
0a98:0080 |
| 3 | STAGE03.EXE |
71,552 | 0x02f0 |
181 | 0f54:001c |
1149:0800 |
| 4 | STAGE04.EXE |
355,136 | 0x0600 |
265 | 0000:0000 |
564c:0080 |
| 5 | STAGE05.EXE |
92,192 | 0x0400 |
119 | 0000:0000 |
1631:0080 |
| 6 | STAGE06.EXE |
259,792 | 0x0200 |
92 | 0000:0000 |
3f45:0080 |
| 7 | STAGE07.EXE |
365,824 | 0x0200 |
107 | 0000:0000 |
5928:0080 |
| 8 | STAGE08.EXE |
86,672 | 0x0200 |
68 | 0000:0000 |
1501:0080 |
Stages 2 and 4 through 8 use a Borland-style startup path. The EXE entry first runs runtime initialization, then far-calls the part body. Stage 3 uses a Microsoft runtime path. The useful body offsets are:
| Stage | Runtime far-call target | File offset in repaired EXE | Main evidence |
|---|---|---|---|
| 2 | 0250:0145 |
0x2845 |
Mode X setup, greetings strings, plane maps |
| 3 | 0d8b:0981 |
0x0e521 |
perspective point loop, bitmap/palette strings |
| 4 | 0216:0021 |
0x02781 |
fullscreen mapper and timer hook |
| 5 | 0258:0000 |
0x02980 |
CRTC transition and palette fades |
| 6 | 00f4:0003 |
0x01143 |
text kernel, SCROLLERS SUCKY string |
| 7 | 01c4:0005 |
0x01e45 |
particle/object records and RLE renderer |
| 8 | 01ca:0002 |
0x01ea2 |
outro/wave/palette stage |
Wrapper And Stage Dispatch
The top MZ entry is deliberately small:
mov sp, 0074h
ret
The return target is arranged by startup data and quickly reaches a far jump through a pointer in the loader's code segment. The wrapper contains the kind of code expected in an early-1990s multi-stage DOS demo loader:
- BIOS services for video and disk checks.
smsw/machine-state tests, probably to reject bad protected/V86 setups.- Self-modifying loops and small trampoline tables.
- The SGI wrapper marker immediately after the MZ header.
- Embedded offsets and data references into the rest of the large EXE.
I am not treating this as a "demo engine" in itself. The visible parts below are separate MZ images. The wrapper's job is closer to orchestration: prepare memory, enter/leave stage programs, and feed them their data.
Embedded Stage 1: Small PKLITE Program
The first embedded PKLITE MZ is only 5,317 bytes on disk and was not repaired into a useful full EXE by the DOS-side unpacking step used here. Its visible strings are mostly runtime/PKLITE material, including the PKWARE copyright, Microsoft runtime fragments, and error-style text such as "Not enough memory".
Because this pass did not recover a clean body for that program, I am not assigning it a visual effect. The substantial visual/control disassembly begins with embedded stage 2.
Stage 2: Greetings/Trug Scroller And Mode X Base Layer
Stage 2 is compact but very revealing. Its strings include:
GREETINGS
GO TO
RENAISSANCE
TRITON
SONIC
sini2.dat
trug_0.map
trug_1.map
trug_2.map
trug_3.map
monster.u
monster.pal
That matches the PANIC.NFO credit for Trug's greetings scroller. More
importantly, this stage contains reusable VGA code that appears, with minor
address changes, in later parts.
Mode X Setup
The mode setup routine around file offset 0x273f starts from BIOS mode 13h
and then reprograms VGA sequencer, graphics-controller, and CRTC registers.
mov ax, 0013h
int 10h
mov dx, 03c4h
mov ax, 0604h ; sequencer memory mode: unchain/odd-even control
out dx, ax
mov ax, 0f02h ; enable all four VGA planes
out dx, ax
mov dx, 03ceh
; writes graphics mode and misc graphics registers
mov dx, 03d4h
; writes max scanline, offset, underline, and mode-control registers
The essential point is that it leaves the screen in a planar 256-color mode:
one byte address selects a group of four horizontal pixels, and the sequencer
map-mask register selects which pixel plane receives the write. That is why
the inner loops below repeatedly hit port 0x3c4, index 2.
Vertical Retrace Wait
The retrace wait is the standard status-port loop:
mov dx, 03dah
wait_out:
in al, dx
test al, 08h
jne wait_out
wait_in:
in al, dx
test al, 08h
je wait_in
This waits first for the current vertical retrace to end, then for the next one to begin. Later stages use this before palette upload, page/CRTC changes, or expensive visual swaps.
Single-Pixel Plotter
The pixel plotter at file offset 0x27cc is the canonical Mode X formula:
offset = line_table[y] + (x >> 2)
plane = 1 << (x & 3)
A000:offset = color, with sequencer map mask set to plane
The code takes x, y, and color as stack parameters. It loads ES=A000,
uses a row-offset table for y, divides the x coordinate by four with a shift,
sets VGA sequencer index 2 to one of 1,2,4,8, and writes the byte.
In pseudocode:
void putpixel_modex(int x, int y, unsigned char color) {
unsigned off = row_offset[y] + (x >> 2);
unsigned mask = 1 << (x & 3);
outw(0x3c4, 0x0200 | mask);
*(unsigned char far *)(0xa0000000 + off) = color;
}
That function is not fast enough for a whole screen, but it is perfect for points, particles, and edge cases. Bulk drawing uses plane blitters instead.
Palette Upload And Readback
The byte palette setter at 0x2803 writes a single DAC entry:
mov dx, 03c8h
out dx, al ; palette index
inc dx ; 03c9h
out dx, r
out dx, g
out dx, b
The bulk helpers are more interesting. One routine writes 0xc0 bytes from a
memory palette through port 0x3c9; another reads 0x300 bytes back from the
DAC. Reading the DAC is useful when fading from the current hardware palette
rather than from a hard-coded source table.
mov dx, 03c8h
xor al, al
out dx, al
inc dx
mov cx, 00c0h
rep outs byte ptr dx, [si]
and:
mov dx, 03c7h
xor al, al
out dx, al
mov dx, 03c9h
mov cx, 0300h
rep ins byte ptr [di], dx
The second count is 768 bytes: 256 colors times three DAC components.
Plane-Deinterleaved Blitter
The blitter at 0x3553 copies a linear source image into planar VGA memory by
walking the four planes separately:
for plane in 0..3:
map_mask = 1 << plane
si = source + plane
di = destination
repeat count / 4 times:
A000[di++] = *si
si += 4
The assembly shape is:
out 03c4h, 0201h
; copy bytes source+0, source+4, source+8...
out 03c4h, 0202h
; copy bytes source+1, source+5, source+9...
out 03c4h, 0204h
; copy bytes source+2, source+6, source+10...
out 03c4h, 0208h
; copy bytes source+3, source+7, source+11...
Inside each pass the loop does a byte copy, increments DI by one, and adds
three extra bytes to SI, making the effective source step four. This is the
inverse of chunky framebuffer order. It converts a conventional linear bitmap
into Mode X's four interleaved planes without doing per-pixel map-mask changes.
Palette Ramp Construction
The stage builds RGB ramps rather than only loading fixed palette data. Around
0x2952..0x29f0, it fills three 128-entry tables using small integer
multipliers and divisions:
red-ish ramp: factor * 9 / 9
green-ish ramp: factor * 8 / 9
blue-ish ramp: factor * 7 / 9
The exact table addresses differ by data segment, but the pattern is clear: construct brightness curves, rotate offsets through them, and later scale the working 768-byte palette by a brightness value divided by 64. The code avoids floating point entirely. It is all byte tables, word multiplies, and integer division.
Four Map Files Into Four VGA Planes
The file strings trug_0.map through trug_3.map are not accidental. Around
0x2a82..0x2afb, the code opens/loads four map files with the map mask set to
1, 2, 4, and 8, copying about 0xfa00 bytes to A000 for each plane.
That is a very direct data layout: each file can already be prearranged for one VGA plane. Runtime loading then becomes port-select, read bytes, copy. The cost of converting from chunky to planar is paid offline or in the packer, not in the frame loop.
Stage 3: Perspective Point Field / Particle Stage
Stage 3 carries these useful strings:
wellpal.pal
window.pal
pbuf1.pre
pbuf2.pre
map.pal
map.u
dep.u
MS Run-Time Library - Copyright (c) 1990, Microsoft Corp
Unlike most other stages, this one uses a Microsoft runtime path. Its
main-like body is reached around file offset 0x0e521.
Bitmap Copy Loops
Two routines at about 0x0b39c and 0x0b3c4 copy rectangular regions from an
offscreen segment to A000. The core shape is:
for row in 0..148:
copy 0xa0 words ; 320 bytes
source += row_step
dest += row_step
One version copies forward from source offset 0x3fc0. Another starts near
0xba40 and subtracts 0x280 per row, so it is drawing from a flipped or
windowed source region. The important detail is that this is not a Mode X
plane operation. It is a plain 320-byte-wide copy, suitable for mode 13h-like
or offscreen linear buffers.
Palette Ramp
At about 0x0d84c, the code writes a 64-step gradient to the DAC, then blacks
the remaining 0xc0 entries. This makes palette color index itself a depth or
brightness value:
for i in 0..63:
DAC[i] = gradient(i)
for i in 64..255:
DAC[i] = black
The point renderer below uses 0x3f - (z >> 8) as a shade, so the palette ramp
turns a cheap z value into a visible brightness gradient.
1024-Point Perspective Inner Loop
The central loop starts around 0x0d872 and iterates 0x400 times. Each
record is table-driven: phase values are advanced, projected with integer
division, clipped, old pixels are erased, and a new pixel is drawn.
The loop is essentially:
for (int i = 0; i < 1024; i++) {
zphase[i] += dz;
int z = (zphase[i] & 0x3fff) + 0x100;
xphase[i] += dx;
int xcentered = (xphase[i] & 0x3fff) - 0x2000;
int sx = xcentered * scale / z + 160;
yphase[i] += dy;
int ycentered = (yphase[i] & 0x3fff) - 0x2000;
int sy = ycentered * scale / z + 100;
erase(oldpos[i]);
if ((unsigned)sx <= 319 && (unsigned)sy <= 199) {
unsigned pos = row_offset[sy] + sx;
unsigned char shade = 0x3f - (z >> 8);
screen[pos] = shade;
oldpos[i] = pos;
} else {
oldpos[i] = 0;
}
}
The table references in the disassembly line up with that interpretation:
- Three phase arrays are read and written with a
BX-relative index. - A separate old-screen-position array is used for erasing previous dots.
- A row-offset table converts
yto a screen address. - Increment variables are stored near the loop and adjusted outside it.
- The scale value is loaded once and used by
imul/idivprojection math.
This is a classic 386-era compromise. The loop pays for signed integer division twice per point, but only for 1024 points. It avoids clearing the whole screen by erasing just the previous pixel for each point. It also uses the palette as its lighting model, so the draw step is one byte store.
The visual role is likely a tunnel, starfield, well, or depth cloud connected
to the wellpal.pal/window.pal strings. The inner loop itself is definitely
a perspective point field.
Stage 4: Fullscreen Texture Mapper And Timer Stage
Stage 4 is much larger after decompression: 355 KB. Its main body starts around
file offset 0x2781. It contains familiar Mode X setup routines, but its
distinctive code is a fullscreen unrolled sampler and a timer interrupt hook.
Timer Interrupt Hook
Around 0xdd7a, stage 4 saves and replaces the timer interrupt vector. The
setup sequence is:
int 21h ah=35h ; get old int 08h vector
store old segment:offset
int 21h ah=25h ; install new handler
out 43h, 36h ; PIT channel 0, mode 3
out 40h, divisor_lo
out 40h, divisor_hi
The handler saves registers including segment registers, calls the stage's
tick/draw callback, sends an EOI with out 20h,20h, restores state, and
returns from interrupt.
That is not just a timer for music. In this stage it also provides deterministic frame pacing and possibly raster/page state changes. Using IRQ0 lets the code update counters even while the foreground loop is busy.
Transform/Angle Update
The main loop around 0x2814..0x28e3 updates angle variables masked to
0x3ff, looks up sine/cosine-like values in 1024-entry tables, and writes
derived fixed-point transform components to a data block.
The shape is:
angle0 = (angle0 + delta0) & 0x3ff
angle1 = (angle1 + delta1) & 0x3ff
angle2 = (angle2 + delta2) & 0x3ff
s0 = table[angle0]
s1 = table[angle1]
s2 = table[angle2]
component0 = fixed_mul(s0, s1)
component1 = fixed_mul(s1, s2)
component2 = fixed_mul(s2, s0)
Those components feed the sampler. The code also cycles a base/page expression
using (frame & 3) << 10, which is consistent with Mode X page/buffer offset
management.
Fullscreen Unrolled Sampler
The key renderer starts around 0xec14. It sets ES=A000, prepares source
position and fixed-point increments, then draws 200 rows. Each row writes 320
pixels as 160 word stores.
The inner row is heavily unrolled. A representative repeated unit is:
mov al, [si]
add bx, bp
adc si, dx
mov ah, [si]
add bx, bp
adc si, dx
mov es:[di+N], ax
Interpreted as fixed-point sampling:
unsigned frac = start_frac;
unsigned src = start_src;
for (int x = 0; x < 320; x += 2) {
unsigned char p0 = texture[src];
frac += frac_step;
src += int_step + carry(frac);
unsigned char p1 = texture[src];
frac += frac_step;
src += int_step + carry(frac);
*(uint16_t *)&screen[row_base + x] = p0 | (p1 << 8);
}
The BX register is the fractional accumulator. BP is the fractional step.
DX is the integer step. ADC SI,DX adds the integer step plus the carry from
the fractional addition. Packing two sampled bytes into AX cuts video-memory
stores in half.
This is the exact kind of inner loop one writes when the CPU bottleneck is
address generation and VGA memory writes. The row math outside the unrolled
body computes source_y * 0x140 + source_x style starting positions; the body
then runs without branches for most of the scanline.
The safest code-side label is "fullscreen fixed-point texture mapper/scaler" or "roto-scaler-style sampler." The complete run above now supplies the visual side: this machinery belongs to the framed blue texture and the red height field that grows through it.
Runtime-To-Code Concordance
The runtime evidence maps cleanly to the stage analysis:
- The logo proves that the wrapper has entered the visible stage sequence, but does not by itself identify a hot renderer loop.
- The framed blue texture and red height field match Stage 4's Mode X setup,
timer hook, row math, and prepared source buffers. The strongest anchors are
the angle update at
0x2814..0x28e3, 1024-entry sine/cosine tables, and the unrolled sampler at0xec14, whereBX,BP,DX, andADC SI,DXform the texture-address walk. - The later GIFs establish the actual order and appearance of the wireframe, colour-field, and particle/outro stages. Their detailed code sections remain conservative where the binary does not provide a source-level effect name.
Stage 5: VGA Transition And Palette-Control Stage
Stage 5's body begins around 0x2980. It is less a heavy renderer than a VGA
transition/control part.
CRTC Wipe/Squeeze
The stage writes several VGA CRTC registers through port 0x3d4, especially:
0x13offset register.0x18line compare.0x07overflow bits.0x09max scanline/double-scan related bits.
The loop around 0x2a84..0x2aaf computes register values from a loop counter
and writes them frame by frame. The effect is a hardware-assisted reveal,
wipe, squeeze, or split-screen transition. This is not just clearing memory;
it changes how VGA fetches and displays the underlying buffer.
Span Copies
The same section calls a far helper with a count around 0x28 and offsets
derived from:
offset = y * 0x50 + 0x3e80
0x50 is 80 bytes, the scanline stride for 320-wide Mode X byte addresses
because four pixels share one byte address. The helper copies spans in A000
while the CRTC registers move the displayed window.
Palette Fade
At about 0x2d4d, the fade routine scales one palette toward another:
out = source * (63 - factor) / 63 + target_or_bias
The operands are byte DAC components, so the routine works over 768 bytes or a subrange of that table. This is the usual VGA demo fade: all expensive color work is done in CPU memory, then the final palette is uploaded during retrace.
Shared Mode X Helpers
Stage 5 also carries the same family of helpers already seen in stage 2:
- Mode X setup from BIOS mode 13h.
- Pixel plotter using
offset = y * 80 + x / 4. - Plane mask from
x & 3. - Four-plane deinterleaving blitter.
That tells us Future Crew is reusing a small VGA support layer across separate part EXEs, not generating every stage from an entirely different codebase.
Stage 6: SCROLLERS SUCKY Text Kernel
Stage 6 is one of the clearest parts. It contains the string:
SCROLLERS SUCKY SCROLLERS SUCKY
The body starts near file offset 0x1143. This stage has a real custom text
kernel, not a simple BIOS font scroller.
Message Preprocessing
The preprocessing loop around 0x11d7..0x1215 walks the message buffer and
maps printable letters into the font/tile code range:
for (int i = 0; i < 0x400 && text[i] != 0; i++) {
unsigned char c = text[i];
if (c >= 'A') {
c += 0xc0;
} else if (c == ' ') {
c = 0;
}
text[i] = c;
}
So the runtime scroller does not repeatedly classify ASCII. The message is converted once into tile IDs, with spaces becoming zero and uppercase letters moving into a high tile range.
Glyph Pointer Table Generation
The next setup loop around 0x1223..0x1315 builds far-pointer tables for glyph
columns. Its nested counters are effectively:
for row_group in 0..1:
for tile_y in 0..11:
for col in 0..5:
source_offset =
row_group * 0x7800 +
tile_y * 0x001a +
col * 4 +
0x1400
store pointer in glyph table
The 0x1400 base and 0x7800 group jump point to a large bitmap/font atlas.
The table stores precomputed offsets so the frame loop can fetch glyph column
pointers directly.
Scroller Driver
The main scroller loop uses a current text position, divides by 26, and uses the quotient/remainder to choose the active character and character column. Then:
char_base = char_code << 6
column = remainder * 2
glyphptr = glyph_table[char_base + column]
call glyph_blitter(glyphptr, destination)
The stage maintains two opposing scroll positions: one counter increments while another decrements. That matches a dual or mirrored text motion. The key point is that the per-frame part does not interpret a bitmap; it indexes a prepared glyph pointer table and calls a specialized blitter.
Glyph Blitter Inner Loop
The glyph blitter at file offset 0x1780 is the classic inner loop in this
stage. It draws into Mode X in four plane passes:
out 03c4h, 0201h
; draw plane 0
out 03c4h, 0202h
; draw plane 1
out 03c4h, 0204h
; draw plane 2
out 03c4h, 0208h
; draw plane 3
Within each plane, source rows are separated by 0x140 bytes, meaning the
source bitmap is 320 bytes wide. Destination rows are separated by 0x50
bytes, meaning the target is Mode X's 80-byte row address space.
The unrolled copy pattern is easiest to describe by offsets. For each small
column group, the routine writes paired pixels into rows spaced by 0x50:
row 0: source + 0x000 -> dest + 0x04f and dest - 0x001
row 1: source + 0x140 -> dest + 0x0ef and dest + 0x09f
row 2: source + 0x280 -> dest + 0x18f and dest + 0x13f
row 3: source + 0x3c0 -> dest + 0x22f and dest + 0x1df
row 4: source + 0x500 -> dest + 0x2cf and dest + 0x27f
row 5: source + 0x640 -> dest + 0x36f and dest + 0x31f
row 6: source + 0x780 -> dest + 0x40f and dest + 0x3bf
row 7: source + 0x8c0 -> dest + 0x4af and dest + 0x45f
After the group, source jumps by 0x0a00 and destination by 0x0500. That is
ten chunky source rows and ten Mode X destination rows. The paired writes
around each row's centerline are what give the text its thick/mirrored stroke
shape.
In structured pseudocode:
for (int plane = 0; plane < 4; plane++) {
outw(0x3c4, 0x0200 | (1 << plane));
unsigned char *src = glyph_src + plane_adjust[plane];
unsigned char far *dst = screen_dst;
for (int group = 0; group < 10; group++) {
for (int row = 0; row < 8; row++) {
unsigned char p = src[row * 320];
dst[row_pair_a[row]] = p;
dst[row_pair_b[row]] = p;
}
src += 0x0a00;
dst += 0x0500;
}
}
The real code is more compact and more unrolled than this. The point is that it pays the plane-switch cost only four times, then streams repeated glyph columns into fixed offsets.
Offscreen Buffer And Copy
Stage 6 also allocates and clears a 64 KB offscreen segment:
mov ah, 48h
mov bx, 1000h ; 0x1000 paragraphs = 64 KB
int 21h
The clear loop uses rep stosd with 0x4000 dwords. The copy-to-screen loop
uses rep movsd with the same count. That is a whole 64 KB buffer:
0x4000 dwords * 4 = 0x10000 bytes
This is a practical arrangement for a text effect: render complex glyphs into an offscreen buffer, then blast the buffer to A000. It avoids visible partial updates while the text blitter is doing many small plane-aware writes.
Timer/Raster Support
The stage programs PIT channel 0 and installs a timer ISR near the end of the
code. A small routine also waits on VGA status and writes CRTC register 0x13
with 0x50, which is the 80-byte Mode X row stride. Together these routines
tie the scroller to predictable frame timing and stable VGA display fetch.
Stage 7: RLE Renderer And Projection Stage
Stage 7's body starts around 0x1e45. It combines two patterns: a small
record-based projection loop and a much more interesting RLE-to-Mode-X
renderer.
Record Initialization
The stage initializes 256 records. Important fields sit at fixed offsets in each record:
+0x35c x-like fixed-point value
+0x35e y-like fixed-point value
+0x360 depth/life value
+0x362 previous/projected screen x
+0x364 previous/projected screen y
The exact base address is segment-relative, but the layout is consistent. This is small-object state: position, depth, and previous draw location.
256-Item Projection Loop
The update loop around 0x22da repeats 256 times:
for (int i = 0; i < 256; i++) {
erase_previous_if_visible(record[i]);
record[i].depth--;
int sx = record[i].x / record[i].depth + 160;
int sy = record[i].y / record[i].depth + 100;
if ((unsigned)sx <= 319 && (unsigned)sy <= 199) {
draw_projected_point_or_sprite(sx, sy);
record[i].oldx = sx;
record[i].oldy = sy;
} else {
mark_invisible(record[i]);
}
}
The real code uses signed idiv by the depth field, then adds 0xa0 and
0x64, which are screen center coordinates 160 and 100. This is a smaller
cousin of the 1024-point loop in stage 3.
RLE-to-Mode-X Renderer
The inner loop at 0x28cf is more distinctive. It decodes a signed-byte
run-length stream and writes directly into planar A000 memory.
The stream commands are:
n < 0: skip -n pixels forward
n = 0: end of stream
n > 0: draw n pixels of the following color
For a single-pixel positive run, it computes the plane mask from di & 3,
writes the sequencer map mask, stores one byte, and advances.
For longer runs, it splits the run into three parts:
- A prefix that reaches a byte-aligned Mode X address.
- An aligned middle written with all four planes enabled.
- A suffix masked to the final partial byte.
The high-level version is:
for (;;) {
int n = (signed char)*src++;
if (n < 0) {
di += -n;
continue;
}
if (n == 0) {
break;
}
unsigned char color = *src++;
if (n == 1) {
outw(0x3c4, 0x0200 | plane_mask[di & 3]);
screen[di >> 2] = color;
di++;
continue;
}
draw_prefix_until_plane_aligned(&di, color);
outw(0x3c4, 0x0f02); // all planes
fill_aligned_bytes_or_words(&di, color, n_middle);
draw_suffix_with_plane_mask(&di, color);
}
The actual implementation has small mask tables in the code segment. The
prefix uses a table near cs:0x0a, the suffix uses a table near cs:0x06,
and the aligned middle sets the map mask to 0x0f with:
mov ax, 0f02h
out dx, ax
Then it fills the middle with duplicated color bytes, using stosb/rep stosw
depending on alignment and count. That is the correct optimization for Mode X:
if the run covers all four planes for one or more byte addresses, one write can
set four horizontal pixels. Only the ragged start and end need plane masks.
This is a real sprite/shape renderer, not a simple bitmap copy. It combines compressed source data with VGA planar run handling, which saves both disk space and CPU time on transparent spans.
Stage 8: Outro/Wave/Palette Stage
The stage 8 body starts around 0x1ea2. It begins by waiting on a timer service
through int 60h, clears A000, then writes a cluster of CRTC registers:
0x09, 0x11, 0x06, 0x0a, 0x18, 0x07, 0x13
The register set points to a display-window or raster-boundary manipulation stage rather than a plain framebuffer effect.
Palette Fade
Around 0x1faf..0x204b, the code fades a small color range. It loops over
roughly color indices 1 through 19 and scales RGB triples from a table by a
frame factor divided by 63:
component = table_component * factor / 63 + background_component
The loop writes the result through a shared DAC upload helper. This is a controlled partial-palette fade, not a whole-screen 256-color fade.
Wave Table Setup
The setup loop around 0x2086..0x20c7 iterates over 200 rows. It indexes a
sine-like table, scales the result, adds 0x20, and calls a helper with a
destination based on:
0x3840 + row
That looks like a per-row displacement or column table. The main animation loop
updates several phase counters and calls a render helper at 0x228a. Without a
runtime image trace I would not claim the exact visual, but the code shape is a
row-wave/palette outro stage.
Exit Wipe
The exit section waits for vertical retrace, uploads the palette, calls the
timer service, and advances CRTC line-compare register 0x18 from about
0xea toward 0x190, with overflow bits adjusted through CRTC register 7.
That is a hardware scroll/wipe: the framebuffer can stay largely unchanged
while the displayed boundary moves.
Shared VGA Idioms Across Stages
Panic's visual parts repeatedly use the same low-level ideas:
- Start from BIOS mode 13h, then unchain VGA into a planar 320-wide Mode X style mode.
- Use sequencer map mask index 2 for plane selection.
- Treat the screen as 80 bytes per row when writing planar addresses.
- Use
0x3davertical-retrace waits around palette and CRTC-sensitive work. - Build/fade palettes in memory and upload through
0x3c8/0x3c9. - Preconvert large images into plane-friendly layouts where possible.
- Use offscreen 64 KB buffers when a part needs many small writes.
- Use fixed-point integer math for projection, texture stepping, and wave motion.
The important architectural point is that Panic is not one renderer with a single loop. It is a chain of small real-mode programs. Some parts share helper code, but each stage is allowed to own its memory layout, data files, and inner loops.
What The Inner Loops Say About The Target Machine
The release text's machine guidance matches the code:
- The demo expects a fast VGA because the heavy stages write directly to A000 and reprogram CRTC/sequencer state often.
- The music system wants EMS and Sound Blaster support, but visual stages are still real-mode VGA code rather than a big protected-mode flat-memory engine.
- The tight loops are 16-bit real-mode code using integer arithmetic and careful VGA plane handling.
- The expensive per-pixel work is avoided whenever possible. Whole bitmaps are plane-deinterleaved, sprites are RLE-compressed with transparent skips, points erase only their previous pixels, and palettes carry brightness.
This is why the demo feels technically dense for 1992/early 1993 without looking like a later DOS extender production. It leans hard on VGA hardware behavior, precomputed data, packed stage executables, and short custom loops.
Limits Of This Pass
I did not reconstruct the wrapper's full stage-dispatch protocol or run a symbolic trace of every visual part. The stage roles are named conservatively: when a label is inferred, this writeup says so. The strongest claims are the ones tied to direct code evidence: Mode X setup, plane blitters, DAC upload, 1024-point perspective loop, timer IRQ hook, fullscreen fixed-point sampler, text glyph kernel, and RLE-to-Mode-X renderer.
Instruction-level traces synchronized to the complete run would still be useful for stages 5 and 8. The direct runtime now establishes their position and appearance, but does not make every inferred binary-to-effect boundary exact.
Appendix: Stage Hashes
Original carved stage hashes:
d6d708a9284dfab3608f2ff3bea169570ea34509a81ac496f8ffe2da40cbdaf0 stage01.exe
2f62b5359e466833e3c0ef7f6c49433889074ca0a4a266aba1696bdcb7e50cb1 stage02.exe
ce60da79acdb68772d911d029968b2dbc421ab1802f3c10cf1829660790218a3 stage03.exe
09e9e8630c25371a51af2f5234034ba5e339a3273e40fb002b56086f300278ff stage04.exe
cf807bb8ec00fc914064c8193a3fb670bdc2349ce49c13bf3939d745d6c31b34 stage05.exe
3b4e7b10f53d11bee0fb4710b4721046022aa40a5a90e1bdd594314f135a6743 stage06.exe
c3beac45264989807b6a486990898fcdce753ae621f0fa35bf0fe3dc28783ee9 stage07.exe
963b934351cf096731d53e257cd4c0e224c5ec20548a5474d84a27a77af47293 stage08.exe
Repaired DOS-side PKLITE-unpacked hashes:
f616122c7b6f7ac8a151f83dafd081c2a6f888c40696abc0a1c9f7b1a6c2d429 STAGE02.EXE
6c157d83831d9d3d1307bb884b15809f5d4536e3af2f8ca37aea676833342511 STAGE03.EXE
4bc62419eef28b4cc5ea5e8ad1d548d63107f4a6d962a82018aaca752860587b STAGE04.EXE
2028813cc43f9e2e843f49127a33e2159554e8bfe8d69af55d5bb02690751398 STAGE05.EXE
a5ed2afd315ab3d1a1ab74147d2dec1b7d7dde6208b05d49bdd6f45e410b10dc STAGE06.EXE
64b07f9acb96f2e681172674d9e959fd42b54246281ace5bcd6e7ec78f0269ec STAGE07.EXE
782e31a0a09c51297b4555dae76443e4b9d99b9e48c2cd11eeea5ea6ce8e67f5 STAGE08.EXE