Analysis model: gpt-5.5 xhigh

Placebo by Complex - Technical Dissection

Scope

Placebo is a 1994 MS-DOS 64K intro by Complex. Pouet uses the Komplex spelling for the group, while Demozoo and the Assembly result file list Complex. I use Complex below because that is what the party result file and the intro's own text use.

Release year: 1994. Demozoo lists the release date as 7 August 1994. Assembly 1994 placed it fourth in the PC 64KB Intro competition, behind AirFrame, Space Jam, and Fyvush.

Public references:

The Assembly 1994 result file lists:

PC 64KB Intro Competition
  1, AirFrame   by Prime
  2, Space Jam  by Fascination
  3, Fyvush     by Jamm
  4, Placebo    by Complex

This writeup is based on the party archive, unpacking the embedded RNC method 1 payload, static 16-bit and 386 disassembly, and byte-level checks against the expanded image. It is not a source-level reconstruction. Routine names are descriptive labels inferred from offsets, constants, loops, data flow, and call sites.

Credits And Visible Script

The intro itself contains a compact script block beginning around expanded payload offset 0x0e60. The important visible strings are:

#COMPLEX
#MAKES IT POSSIBLE
#PRESENTS
#PLACEBO
#INTRO
N-JOY!
#*REAL*
#SPLINE
#RUBBER
#TEXTURE-MAPPING
#LARUSSE SHADING
TERVE TEILLE BITTI-NUORET!
CAN YOU FEEL THE WORLD DOMINATION...
MUSIC BY JUGI
CODE BY JMAGIC
GFX BY JUGI AND REWARD
LET'S TAKE A LOOK AT THIS ANIM. BY SIRE
256*256
10 FRAMES
TOTAL#640KB#OF#DATA!

Demozoo lists Jmagic for code, Jugi for graphics and music, Reward for graphics, and Sire for the animation. Pouet lists Jmagic for code and Jugi for music. The expanded payload agrees with that division: most visual content is embedded, the module-like music data is present near the end of the payload, and the final animation subsystem is distinct from the earlier spline and texture routines.

Runtime Capture

A DOSBox-X 2026.01.02 runtime capture on 19 June 2026 used the original COMPO1.EXE, the GUS configuration ULTRASND=240,3,3,5,5, and the intro's own GUS-port prompt with 240h selected. Timing zero is the start of the capture stream, not the first visual frame, so the first few seconds are the DOSBox-X shell and the sound setup screen. The useful visual run begins once the RNC loader, FlatMan setup, and music player have handed off to the scripted show.

The sequence GIF below is assembled directly from the eight timestamped DOSBox-X frames in this section. It compresses the visible script into a single motion index: Complex identity card, Placebo title, rubber-textured object, feature-list text, Finnish/world-domination text, and the late planar animation path.

Placebo runtime sequence GIF showing title cards, rubber object, feature text, and final planar animation

At 00:07.000, the intro has reached the first clean identity card. The face bitmap is already in place while the script renderer writes the COMPLEX PRESENTS text beside it.

Placebo runtime frame at 00:07.000 showing the Complex Presents card and face bitmap

At 00:12.000, the same script has advanced to the title. This is the visible counterpart of the embedded #PLACEBO / #INTRO strings near the front of the expanded payload.

Placebo runtime frame at 00:12.000 showing the Placebo Intro title and blue face bitmap

By 00:20.000, the title cards have yielded to the rubber/spline object pass. This is the part that makes the later rotation, face-selection, and texture span code worth reading: the object is not a flat logo transition, but a small distorted surface whose faces and texture fragments keep being rebuilt.

Placebo runtime frame at 00:20.000 showing the first distorted rubber-textured object

At 00:28.000, the intro names its own claims: *REAL*, SPLINE, RUBBER, TEXTURE-MAPPING, and LARUSSE SHADING. The object is still live on the right, so this frame ties the boast text directly to the renderer rather than leaving it as a static credit card.

Placebo runtime frame at 00:28.000 showing the feature-list text and rotating rubber object

The 00:39.000 frame is the longer text-and-object phase. The Finnish greeting and CAN YOU FEEL THE WORLD DOMINATION... line are being drawn while the same warped object continues to move. This is the most compact visual anchor for the music-timed script, object transform, palette, and final copy routines working together.

Placebo runtime frame at 00:39.000 showing the Finnish greeting and world-domination text beside the object

The final animation begins as a mostly white planar screen. At 01:00.000, the first green strokes have started to accumulate, which is a useful boundary frame: the renderer has moved away from the chunky 256-color object scene and into the separate mode-0Dh animation path described later.

Placebo runtime frame at 01:00.000 showing the first green strokes of the final animation

At 01:03.000, the animation has filled enough of the screen to show its painted, stippled character. It reads like a large stored picture, but the code below shows the compact strip/frame data and mask-table compositor behind it.

Placebo runtime frame at 01:03.000 showing the colorful final animation filling the screen

At 01:11.000, the final animation is in its denser phase. The capture stream used for these screenshots runs for 76.5 seconds and was stopped externally after this section was already established, so these timestamps should be read as capture-stream anchors rather than proof of the intro's exact exit timing.

Placebo runtime frame at 01:11.000 showing the late colorful final animation state

Runtime-To-Code Concordance

The published GIF spans the three main visual families: script/title cards, the rubber/textured object, and the final planar animation. The code matches that split cleanly because Placebo is music-counter gated rather than a loose delay-loop slideshow.

00:07.000  COMPLEX PRESENTS card with face bitmap
           the MZ/RNC/FlatMan setup has reached the main scheduler at 0x2968.
           The visible text is the script renderer at 0x266a drawing into
           A000h with bright font pixels shifted into the 0xf8 palette range.

00:12.000  PLACEBO INTRO title card
           this maps directly to the embedded #PLACEBO / #INTRO strings near
           expanded payload offset 0x0e60. The scheduler consumes multiple
           script bytes per visual tick around 0x2c09..0x2c15 and
           0x2f82..0x2fbb, paced by the GUS/player counter at 4122:[0f4e].

00:20.000  first rubber/textured object
           control has moved into the spline/rubber section: 3-axis fixed-point
           rotation at 0x1f84, projection at 0x206e, and face/page selection at
           0x20cf prepare the object seen on the right.

00:28.000  feature-list text with live object
           the boast text and object share the frame. Text still comes from the
           0x266a script path, while the object uses quad setup at 0x19f7 and
           the eight-pixel unrolled texture span loop at 0x18a3.

00:39.000  Finnish/world-domination text with object
           this is the compact evidence for the music-timed scheduler: text
           bursts, palette stepping, object rotation/projection, flat
           work-buffer clear at 0x244e, and visible VGA strip copy at 0x25ba
           are all active in one scene.

01:00.000  final animation begins as green strokes
           the show has switched renderer families. The final subsystem enters
           mode 0Dh through the code area around 0x3e790 and begins consuming
           compact frame-strip data rather than the earlier 256-color texture
           mapper.

01:03.000  colorful final animation filling the screen
           the frame-strip loader at 0x3eee1, row builders at 0x3e8cd/0x3eab1,
           and bit compositors at 0x3ec8a/0x3ed3f are now the visible engine.

01:11.000  dense final animation state
           plane output at 0x3e7c4 writes generated planar data to A000h using
           VGA plane-select logic. Timing still follows the music/player state
           and local animation ticks before fade-out and lret.

The important boundary is that the rubber object and final animation are not the same renderer. The rubber section builds a chunky-ish flat work buffer, maps texture spans into it, and copies selected strips to visible VGA. The late animation switches to planar mode 0Dh, builds mask tables, expands compact strips, merges bit offsets, and writes VGA planes directly. The runtime frames show that handoff clearly: from a live textured object beside script text to a separate full-screen painted/stippled animation.

Examined Files

Archive:

8216f1d87fe839669632b8ed3b8a273ad16104aedbcef2a77e57e7ddae0f0f92  placebo.zip

Extracted file:

df5c139c35e1af813a882f6cefcac189b88d59b25516e276be4b9b8c9073ac79  COMPO1.EXE

Expanded RNC payload:

ecdb8c50d6c74b7064678108685c803974edf5ab25fc29da8989eeb555caf398  placebo_unrnc.bin

Scene.org's archive preview contains only COMPO1.EXE. The MZ wrapper is 53,878 bytes long, and the expanded runtime image is 277,193 bytes long.

MZ Wrapper

The executable is a tiny MZ shell with an RNC payload starting at file offset 0x20.

MZ signature:       5a4d
file size:          53,878 bytes
header paragraphs: 2
relocations:        0
minalloc/maxalloc:  0x36da / 0xffff paragraphs
entry CS:IP:        0x0cf3:0x000c
entry file offset:  0xcf3c
initial SS:SP:      0x43ae:0x0526

The header shape is typical for a small real-mode loader:

The loader's job is to place the packed block somewhere safe, call the RNC depacker, set up the segment state expected by the expanded runtime, and then transfer control into the expanded image.

RNC Payload

The RNC stream starts at COMPO1.EXE + 0x20.

method:          1
packed size:     53,014 bytes
unpacked size:   277,193 bytes
packed CRC:      0x233c
unpacked CRC:    0xa0d0
leeway:          0
chunks:          24
ratio:           about 19.1 percent

The stream has the RNC "locked" bit set but does not set the encryption bit:

first flag / lock bit:       1
second flag / encryption:    0
next word after flags:       0x624a

That is why a generic command-line unpack refuses the file as locked even though the bytes are not encrypted. Reading the two flag bytes and then feeding the remaining method-1 stream into the unpack core produces a 277,193-byte image with the expected 0xa0d0 CRC.

The expanded image contains:

FlatMan And Memory Model

The expanded image starts with FlatMan:

0x00f7  "FLATMAN Version 0.3"

The early setup checks for XMS and then establishes a flat real-mode memory view. The important path is:

0074:  mov ax,4300h
       int 2fh           ; XMS installed check

0084:  mov ax,4310h
       int 2fh           ; get XMS entry ES:BX
       mov cs:[00f3],bx
       mov cs:[00f5],es

... later calls through the saved XMS entry ...
       lcall far [cs:00f3]

The CPU setup path begins around 0x028f. It uses pushf / popf checks to reject non-386 CPUs, then uses smsw, mov cr0, lgdt, and selector loads to create flat real-mode segment limits:

load GDT descriptor
set CR0.PE
load DS/ES/FS/GS with selector 0008h
clear CR0.PE
return to real-mode execution with flat segment limits still cached

This is the central trick that lets later real-mode-looking code use 32-bit registers and large linear pointers. It gives the intro room for the animation work buffer while still using direct VGA ports and real-mode style far calls.

The demo-facing setup around 0x2968 then resizes DOS memory:

mov ah,4ah
mov bx,46ceh
int 21h

Soon after that it performs a placeholder far call:

mov ax,0000h
mov bx,0617h
db  9ah,00h,00h,00h,00h
mov cs:[0a00],eax

The zero far pointer is almost certainly patched or resolved by the loader's relocation/service path. The returned EAX is stored at cs:0a00 and is used as the main flat work-buffer pointer in later routines.

GUS And Music Timing

The sound setup is concentrated through a code/data segment near 0x4122. The visible user prompt at expanded payload offset 0x3227 is a GUS port selector. It offers the usual values:

210h 220h 230h 240h 250h 260h

The default is 220h. The selected value is stored into:

DS = 4122h
[0f30] = selected GUS port

The main setup and playback path uses far calls into the same segment:

4122:23c4
4122:23a7
4122:2543
4122:22a9
4122:2347
4122:2536

The key timeline variable is FS=4122:[0f4e]. The main script checks it against thresholds such as:

0x0020
0x013f
0x0a00
0x0e00
0x1200
0x1f00
0x2200
0x2710

That counter gates scene changes. The intro is not just delaying for fixed CPU-time loops; it watches the music/player state and enters the next part when the counter reaches the next marker.

The music data begins around 0x34b40 with the title:

Intro

At 0x34b40 + 1080 the module signature is:

8CHN

That identifies it as an 8-channel MOD-family block. The instrument/sample text includes Deepbass jugi, matching the public credit for Jugi's music.

VGA Setup And Retrace Waits

The general retrace helper is at 0x273e:

mov dx,03dah
.wait_not_in_retrace:
    in  al,dx
    test al,08h
    jne .wait_not_in_retrace
.wait_in_retrace:
    in  al,dx
    test al,08h
    je  .wait_in_retrace
ret

That is the standard two-edge wait on input status register 1. It first waits until vertical retrace is clear, then waits for the next retrace start.

There are two nearby VGA setup routines. The first, at 0x28ff, begins with BIOS mode 13h and then touches the sequencer, miscellaneous output register, and CRTC:

mov ax,0013h
int 10h

mov dx,03c4h
mov ax,0100h
out dx,ax              ; sync reset

mov dx,03c2h
mov al,0e3h
out dx,al              ; misc output

mov dx,03d4h
mov al,11h
out dx,al
inc dx
in  al,dx
and al,7fh
out dx,al              ; unlock CRTC register writes

The later mode setup at 0x2ec3 is clearer because each CRTC pair is written explicitly:

mov ax,0013h
int 10h

mov dx,03c4h
mov ax,0100h
out dx,ax

mov dx,03c2h
mov al,0e3h
out dx,al

mov dx,03d4h
out 00h,5fh
out 01h,3fh
out 02h,40h
out 03h,82h
out 04h,4ah
out 05h,9ah
out 06h,23h
out 07h,b2h
out 09h,61h
out 10h,0ah
out 11h,ach
out 12h,ffh
out 13h,20h
out 15h,07h
out 16h,1ah

mov dx,03c4h
mov ax,0300h
out dx,ax              ; leave reset

This is a tweaked 256-color VGA mode derived from mode 13h. The visible code keeps a 320-byte row pitch (CRTC offset = 0x20) and alters timing/vertical registers. Later routines still use A000h directly for many effects, while the heavy texture work renders through the flat work buffer and then copies slices to visible VGA memory.

Palette Ramps And Fades

Routine 0x27bc builds DAC ramps from a compact table near 0x1b64. The shape is:

for each ramp descriptor:
    set starting DAC index through 03c8h
    write RGB triples through 03c9h
    use byte deltas/counts from the table to build the next entries

The fade helpers at 0x27f5 and 0x2826 are keyed by state near cs:2431. They do not write an arbitrary palette every frame. Instead, they step selected DAC ranges toward or away from target values, which is cheaper and fits the intro's staged text/effect transitions.

Routine 0x2875 clears visible bands in A000h:

ES = A000h
repeat 4 bands:
    rep stosd for 0x1e dwords
    advance DI by 0x00c8
increment cs:[24b3]

The incremented state byte is later used as a timing/phase value. The routine is small, but it shows the general style: clear or update only the useful screen strips, then let the music counter and local phase state pick the next step.

Main Scheduler At 0x2968

The main runtime code begins with system setup and then becomes a music-marked sequence of far calls and local effect loops. Its high-level order is:

  1. shrink/adjust the DOS allocation,
  2. acquire a flat work buffer through the FlatMan service path,
  3. ask for or use the GUS port value,
  4. initialize the player,
  5. set a tweaked 256-color VGA mode,
  6. run title text and early palette routines,
  7. enter the spline/rubber/texture section,
  8. call the final planar animation subsystem,
  9. fade out, stop playback, restore mode/text, and exit.

The scheduler reads the music counter from 4122:[0f4e] and repeats small draw calls until a threshold is reached. Examples:

0x2c09..0x2c15: call the text renderer five times in one visual tick
0x2f82..0x2fbb: call the text renderer twenty times in one loop

That is why the script text appears in bursts or smooth blocks rather than strictly one glyph per frame. The text draw function is fast enough that the scheduler can consume several script bytes before waiting for the next visual or music state.

Text Script Renderer At 0x266a

The text system is one of the cleanest parts of the expanded image.

State fields:

cs:22a7  script pointer
cs:22a3  current x
cs:22a5  current y

Control bytes:

0x00  reset script pointer and return
0x0d  carriage return: x = 0, y += 0x10
0x0a  home-like reset: x = 0, y = 0

Normal characters subtract 0x20, then use two tables:

0x0c27 + char  glyph width
0x0c62 + char  x advance

The destination pointer is:

DI = (y + 8) * 320 + x
ES = A000h

The glyph bitmap is stored in a column/row layout with a large row stride. The hot loop is:

ch = 16
for each glyph row:
    cl = glyph_width
    ah = fs:[glyph_bitmap + bx]
    if ah != 0:
        ah += 0f8h
    es:[di] = ah
    di += 1
    repeat cl times

    bx += 02dbh
    di += 0140h
    ch -= 1

Two details matter:

After a glyph, the renderer advances x by the character's advance-table entry. That lets narrow characters consume less horizontal space while still using fixed 16-row glyphs.

Routine 0x28d5 sets the script pointer to 0x0a1c, sets a small frame count of 0x28, and then alternates retrace waits and text draws. The scheduler uses the same primitive in larger call groups for later slogans and credits.

Rotation Helper At 0x1f84

The spline and texture-mapped geometry sections share a fixed-point 3D helper. Angle state lives at:

cs:0a08
cs:0a0a
cs:0a0c

Each is masked with 0x03ff, giving 1024 angle steps. The trig tables are laid out as sine/cosine halves:

base + 0x000  sine-like table
base + 0x200  cosine-like table

The routine repeatedly does:

imul word [trig_entry]
combine DX:AX into a 32-bit intermediate
sar  intermediate,8
store the reduced fixed-point component

The sar 8 is the visible scale normalization. The use of signed multiply and arithmetic shift means the coordinates are signed fixed-point values, not byte-space table offsets.

The helper rotates a point through three axis stages and returns the transformed components in the caller's working registers. The projection routine then uses those results to build screen-space vertices.

Projection At 0x206e

Routine 0x206e projects eight control points. For each point:

  1. load model-space coordinates from tables near 0x1b31,
  2. rotate the point through 0x1f84,
  3. subtract camera/depth offset cs:0a12,
  4. perform perspective divides,
  5. store screen x/y into arrays near 0x1b0f and 0x1b1f.

The projection constants are:

screen_x = (x * 0x01ec) / z + 0x64
screen_y = (y * 0x0155) / z + 0x42

Those center values put the object around (100,66) rather than exactly (160,100), which matches the part's framed/composited look. The routine is not doing a general camera matrix; it is a compact object-space rotation plus perspective divide tuned for a small set of control points.

Face Selection And Texture Pages At 0x20cf

Routine 0x20cf turns the projected points into visible textured faces. It repeats the same edge/face setup four times:

load four projected vertices
compute a signed cross-product / area term
if the sign says the face is back-facing, skip it
otherwise choose a texture page and call the scan converter

The texture page selection is unusual. The code takes the magnitude of the area-like term, adds 0x06a4, multiplies by 3, shifts by 12, masks to a value from 0 to 7, and then multiplies by 1024:

page = (((abs(area) + 0x06a4) * 3) >> 12) & 7
texture_offset = page << 10

That result is added to one of the base texture pointers around:

cs:14df
cs:14e3
cs:14e7

The effect is a coarse shade/texture page selection based on face orientation or screen-space area. This fits the intro's own LARUSSE SHADING joke: the face is not just flat filled, but it is not a physically correct lighting model either. It is a compact page switch feeding the texture mapper.

Before calling the scan converter, the routine packs vertex coordinates and texture endpoint bytes into fields around:

cs:14c7
cs:14cb
cs:14cf
cs:14d3
cs:14d7..cs:14de

Then it calls 0x19f7.

Quad Scan Setup At 0x19f7

Routine 0x19f7 is the edge walker for a textured quadrilateral. It builds per-scanline edge tables near:

0x0c9d + y * 2
0x0e9d + y * 2
0x109d + y * 2
0x129d + y * 2

The exact offsets vary by field, but the pattern is four parallel tables:

For each edge, the setup code:

compares endpoint y values
swaps endpoints when the edge runs upward
negates deltas for a normalized downward edge
computes dx/dy as a 16.16 fixed-point slope with idiv
computes texture-coordinate deltas the same way
writes one table entry per covered scanline

It then finds the y min/max range, clamps it to the visible span 0..0x85, and calls the span filler at 0x18a3 for each scanline. The bottom limit of 0x85 explains why the mapped object is drawn in a restricted band rather than across the full 200-line screen.

Texture Span Inner Loop At 0x18a3

Routine 0x18a3 is the classic inner loop in this intro.

Inputs are the two edge-table entries for the current scanline. It computes:

span length = right_x - left_x
du = (right_u - left_u) / (span length + 1)
dv = (right_v - left_v) / (span length + 1)
destination = flat_framebuffer + x

It returns immediately if the span length is less than one pixel.

The filler then draws in groups of eight pixels. The core pattern is:

; U/V are carried in split registers.
; BX is the texture pointer core.
; ESI/EBP are fixed-point increments.

shr dh,1
adc edx,esi
mov al,[bx]
adc ebx,ebp
mov bh,dl

shr dh,1
adc edx,esi
mov ah,[bx]
adc ebx,ebp
mov bh,dl

es:[edi+0] = ax

The real loop repeats that pair pattern four times, producing eight pixels with four 16-bit stores:

write pixels 0..1 as AX
write pixels 2..3 as AX
write pixels 4..5 as AX
write pixels 6..7 as AX
advance EDI by 8

Then it handles a remaining pair loop and a final single-pixel tail.

The clever part is the way the texture pointer is assembled:

That makes the per-pixel work mostly shifts, adds-with-carry, byte loads, and word stores. There is no multiply in the hot loop and no per-pixel divide. All expensive slope work was moved into 0x19f7.

Work Buffer Clear At 0x244e

Routine 0x244e clears a large flat buffer region used by the rubber/texture section.

It computes a phase-dependent base:

cs:0a04 = cs:1b2f * 0x67e8
EDI = cs:[0a00] + cs:[0a04]

Then it clears 0x86 rows. Each row clears 0xc8 bytes with unrolled 32-bit stores:

row_count = 0x86
for each row:
    write zero dwords through offset 0x00c4
    EDI += 0x00c8

The row size of 200 bytes is not a normal 320-wide screen row. It is a workspace layout for a wrapped or sliced effect. The phase term controls where inside the larger buffer the current frame begins.

Flat Buffer To VGA Copy At 0x25ba

Routine 0x25ba copies selected strips from the flat buffer to visible VGA memory:

DS = 0000h
ES = A000h
ESI = cs:[0a00]
EBX = cs:[0a04]
DI  = cs:[0a14]
cs:[1b2f] += 1

The outer loop runs 0x2c times. Each outer iteration advances the phase by 0x67e8 modulo 0x185a60.

Inside each outer iteration:

repeat 3 groups:
    DI += 0x78
    repeat 5 rows:
        copy 10 dwords = 40 bytes from [ESI] to ES:DI
        ESI += 0x28

Then the phase offset is subtracted back from ESI for the next outer strip.

This routine is not a plain full-screen copy. It is an unrolled strip transfer: 40-byte chunks, repeated in grouped rows, with a large wrapped offset driving which part of the flat buffer becomes visible. That matches the visible cylinder/rubber feel: the heavy drawing happens in a work buffer, while the screen receives phase-shifted slices.

RIX And Other Embedded Assets

There is a RIX3 marker at expanded payload offset 0x31760:

RIX3 02 10 00 af 00 ...

The bytes following it include palette-looking data. That is likely an embedded picture or texture source in RIX format.

Other notable strings and markers:

0x3365   "intro.mod"
0x34b40  "Intro"
0x34c80  "Deepbass jugi"
0x34f78  "8CHN" module signature
0x41220  "M.K.FLT46CHN8CHNOCTA"

The M.K.FLT46CHN8CHNOCTA sequence near 0x41220 is not the start of the main module. It looks more like signature text or asset residue embedded near the player/data block.

Final Animation Subsystem

The animation part is entered through a far call from the main body:

lcall 3e79:07d7

That lands in a separate code area around linear offset 0x3e790. It behaves like a self-contained animation engine rather than another call into the earlier texture pipeline.

The local setup at 0x3ef67 receives EAX as the flat buffer pointer and stores it at cs:0747. It then clears a large workspace:

clear size: 0x192000 dwords worth of loop space in the local clear path
mode:       BIOS 0Dh

Mode 0Dh is 320x200 16-color planar VGA. This is different from the earlier tweaked 256-color path. The animation is therefore a planar bit/mask engine, not a chunky texture mapper.

Mask Table Builder At 0x3e790

The first important local routine builds 32 mask entries:

mask table:        cs:2989
inverse masks:     cs:2a09
entry count:       32

The loop uses 386 bit operations:

shrd
bswap-like byte rearrangement through rotates/swaps
not
store mask
store inverse mask

The masks represent all possible bit offsets inside a 32-bit lane. Later compositors select one mask with bit_offset = x & 0x1f and one word/dword lane with x >> 5.

Frame Strip Loader At 0x3eee1

Routine 0x3eee1 converts a frame index into a source offset:

frame_base = cs:[074b] * 0x1920
cs:[074d] = frame_base

It then copies 0xc9 strips, each 0x20 bytes wide, from local animation data into the flat buffer. The constants are telling:

0x1920 = 6,432 bytes
0xc9   = 201 rows
0x20   = 32 bytes per row/strip

That is a compact bitplane-ish frame layout: about 32 bytes per row for roughly 200 rows, which is exactly the kind of storage you would expect for a 256-pixel wide 1-bit or mask plane.

Animation Tick At 0x3edf6

The animation tick increments a selector at cs:0985 and wraps it modulo 0x7c. It then calls:

0x3e8cd
0x3eab1
0x3ec8a
0x3ed3f

The first two build shifted/interpolated row data. The second two composite bitfields into destination rows with the mask tables.

It does that twice per visual tick in the hot path, which gives the animation a denser update than a naive "load frame, copy frame, wait" loop.

Row Builders At 0x3e8cd And 0x3eab1

These routines read small row buffers around:

0x09e9..0x0a08

and write packed rows around:

0x09c9

The forward routine shifts right through neighboring bytes; the reverse routine uses rotate-left style operations. The effect is half-pixel or bit-offset interpolation between rows/columns before the data is merged into the planar destination.

The important point is that the animation is not just displaying ten raw frames. It loads compact frame strips, generates shifted variants, and then uses the bit compositors to place those strips at arbitrary bit offsets.

Bit Compositors At 0x3ec8a And 0x3ed3f

The compositors take an x-like value and split it:

lane       = x >> 5
bit_offset = x & 0x1f

Then they select:

mask      = cs:2989[bit_offset]
inv_mask  = cs:2a09[bit_offset]

The hot pattern is:

load destination dword(s)
rotate/swap source bits into lane alignment
and old destination with inverse mask
and new bits with mask
or the two parts
store merged destination dword(s)

This is a classic planar/bitplane sprite operation. It lets an object or strip start at any pixel bit position without requiring pre-shifted copies for every possible x value. The table built at 0x3e790 supplies the masks, and the row builders supply the shifted source words.

Plane Output At 0x3e7c4

Routine 0x3e7c4 writes the generated animation state to VGA memory. It uses Sequencer map masks through port 03c4h:

map mask 1-ish path
map mask 2-ish path
map mask 4-ish path

For each active plane pass:

ES = A000h
copy 0xc9 rows
for each row:
    offset = wrapped by 0x1920 modulo 0x192000
    copy 8 dwords = 32 bytes
    DI += 8

The third plane-like pass also copies from a static mask/line table near cs:0989. The result is a compact planar animation display with separate generated planes, not a 64,000-byte chunky frame copy.

Animation Timing And Exit

The local animation loop waits for vertical retrace at 0x3eec9, checks the keyboard port directly:

in  al,60h
cmp al,01h       ; ESC scan code

and watches the same music state region used by the main scheduler. It waits for a music marker around 0x1400, uses markers around 0x1900, and has a local timeout around 0x210 ticks before fading out and returning with lret.

The animation part is therefore synchronized to the music/player timeline just like the text and texture parts, but its renderer is a separate planar engine.

What Each Visible Part Is Doing

The intro's visible sequence maps cleanly onto the code:

Technical Reading

Placebo is small, but it is not simple. The 64K file hides a 277K expanded runtime. That runtime then relies on three different layers:

  1. FlatMan gives the real-mode code a large-memory 386 view.
  2. The GUS/player block provides both audio and a shared timeline counter.
  3. The visual code switches between chunky-ish 256-color work-buffer effects and a separate planar VGA animation engine.

The most important inner loop is the span filler at 0x18a3. It shows the typical high-end 1994 PC intro tradeoff: all divides and visibility work happen per edge or per scanline, while the per-pixel loop is reduced to carry-chain coordinate stepping, texture byte loads, and 16-bit writes. The final animation engine shows a different optimization style: table-built masks and dword merges allow arbitrary bit placement in planar VGA without storing all shifted frames.

That combination explains why the intro feels larger than its archive. It is not just packed art. It is a compact runtime with a real loader, a music-timed script, a texture mapper, and a planar animation system coexisting inside a single 64K competition executable.