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:

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:

Panic logo assembled from metallic and fire-filled tiles

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

Panic concentric black-and-white tunnel with yellow line drawings

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:

Panic framed blue animated texture

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

Panic red animated height field growing through its 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:

Panic blue wireframe object changing form

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

Panic Future Crew stone logo transition

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

Panic smoothly warped red white and cyan colour field

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

Panic particle and face outro

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:

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:

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:

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:

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:

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:

  1. A prefix that reaches a byte-aligned Mode X address.
  2. An aligned middle written with all four planes enabled.
  3. 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:

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:

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