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:
- Demozoo production page: https://demozoo.org/productions/6809/
- Pouet production page: https://www.pouet.net/prod.php?which=1275
- Scene.org archive page: https://files.scene.org/view/parties/1994/assembly94/in64/placebo.zip
- Assembly 1994 results: https://archive.scene.org/pub/parties/1994/assembly94/results.txt
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.

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.

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.

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.

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.

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.

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.

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.

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.

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:
- no relocation table entries,
- a short header,
- a large requested allocation,
- an entry point near the end of the file rather than at the start of the payload.
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:
- the FlatMan loader and flat real-mode setup,
- a GUS-oriented music/player block,
- a script-driven text system,
- a tweaked VGA setup,
- spline, rubber, texture, and shaded geometry code,
- a distinct planar animation subsystem,
- RIX image data,
- an 8-channel module-like music block.
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:
- shrink/adjust the DOS allocation,
- acquire a flat work buffer through the FlatMan service path,
- ask for or use the GUS port value,
- initialize the player,
- set a tweaked 256-color VGA mode,
- run title text and early palette routines,
- enter the spline/rubber/texture section,
- call the final planar animation subsystem,
- 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:
- Nonzero font pixels are not copied as-is. They are shifted into a bright
palette range by adding
0xf8. bx += 0x02dbmakes the font storage column-major or interleaved rather than a simple 8x16 BIOS-font copy.
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:
- load model-space coordinates from tables near
0x1b31, - rotate the point through
0x1f84, - subtract camera/depth offset
cs:0a12, - perform perspective divides,
- store screen x/y into arrays near
0x1b0fand0x1b1f.
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:
- left/right x,
- texture coordinate or fractional value for one axis,
- texture coordinate or fractional value for the other axis,
- auxiliary slope/fraction state.
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:
adc edx,esiadvances one coordinate with carry.adc ebx,ebpadvances the other coordinate and the texture base fraction.mov bh,dlmixes the high byte of one coordinate into the texture pointer.- two sampled bytes are packed into
AX.
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:
- Startup and GUS prompt: real-mode MZ wrapper, RNC depack, FlatMan setup, GUS port selection, player initialization.
- Title and slogans: script renderer at
0x266a, bright palette font pixels, several script bytes consumed per visual tick. - Palette transitions: DAC ramp builder and small fade steppers, usually touching selected ranges rather than rewriting everything blindly.
- Spline/rubber object: fixed-point 3-axis rotation at
0x1f84, projection at0x206e, face selection at0x20cf. - Texture/rubber spans: edge setup at
0x19f7, eight-pixel unrolled sampler at0x18a3, flat work-buffer clear/copy at0x244eand0x25ba. - Animation by Sire: planar mode
0Dh, compact strip/frame data, generated mask tables, bit-offset compositors, and plane-select VGA copies. - End/exit: music-counter gates, retrace waits, palette fade, player shutdown, mode restore.
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:
- FlatMan gives the real-mode code a large-memory 386 view.
- The GUS/player block provides both audio and a shared timeline counter.
- 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.