Analysis model: gpt-5.5 xhigh

Verses by Electromotive Force - Technical Dissection

Scope

This is a static binary dissection of Verses by Electromotive Force, released for MS-DOS at Assembly 1994. Demozoo and Pouet both list it as the 1st-place entry in the Assembly 1994 PC demo competition. In the current repo sequence, this starts filling the Assembly 1994 top-three PC demo set.

Useful public references:

The analysis is based on the released archive, the NFO, UNP-unpacked executable code, the appended resource table, 16/32-bit x86 disassembly, and a public video-reference capture for effect timing. Offsets such as 0x304a6 refer to offsets in the UNP-expanded MZ load image after stripping the executable header. Offsets explicitly named as overlay+... refer to the appended data area after the MZ image. Resource-table offsets are absolute offsets in VERSES.EXE, not overlay-relative offsets.

Examined Files

The archive contains four files:

FILE_ID.DIZ       BBS file description
VERSES.EXE        single executable containing PMODE code and appended data
EMF.NFO           group info
VERSES.NFO        production info, requirements, credits, technical notes

Hashes from the examined files:

42312e5aba5e09471c18c1f347118d40b4e6ec13abd301bb08cbd3af8030f657  emf_vrs.zip
f3784702213a7a40ff8788bd5ac3f550f8050a369cc7a6b2615e2fc957960460  FILE_ID.DIZ
7073c9bb3303bf08e0e387beb7ba07d9e88043ad15300da51d32744de3343a84  VERSES.EXE packed
6726fdc86fd2072b76189a2c1e8426f793317ed47c96158ceee4f20ea6290360  VERSES.EXE after UNP
3268eb6a910830146f59e4fe21bba3e7f1b0e8d1365f3806d024489dbbe0d79e  EMF.NFO
cd6b26396f1cc3de66c78982e5f48e628be3a7af150ebadb534f59f6fd00780d  VERSES.NFO
68d3dd0344184513b0e7bc1cf41107464576f39d6a4bf36990474f9f515f2b5c  67_EMF_-_Verses.avi

The AVI reference is 512x384 MPEG-4 video at 25 fps, duration about 266.08 seconds. The screenshots below use that public capture's timestamps; they are not local DOSBox-X timestamps.

The NFO states that the demo uses a slightly customized PMODE 2.24 by Tran, is coded in assembly, is optimized for i486, needs a 386sx or better, VGA, about 400 KB conventional memory, at least 1 MB extended memory, and a Gravis Ultrasound for music. It explicitly does not include Sound Blaster music support.

Credits relevant to the code/effects:

Saracen          voxel cave, bitmap rotation, ripple, dotfade, kaleidoscope,
                 julia morpher, greetings, credits/frontend routines
The Grim Reaper gouraud routines, design star, IFS morpher, diagonal dotfade,
                 Bill Gates whirl, EMF gouraud logo
Devastator       mosaic, polygon ball, file routines, music routines/frontend
Saint            presentation text, Bill Gates writer/mutation, music routines
Whalebone        music and some graphics

I am intentionally not reproducing old phone/address/BBS contact data from the NFO.

Executable Shape

The original VERSES.EXE is not just a flat DOS MZ program. The packed file has a small initialized MZ image and a large appended data area:

Packed file size:      1,030,549 bytes
Packed MZ size:            86,951 bytes
Packed load image:         86,919 bytes
Packed overlay/data:      943,598 bytes
Header size:                  32 bytes
Relocations:                   1
Entry CS:IP:           FFF0:0100

UNP expands only the MZ image. The appended data stays byte-identical and at the same length:

UNP file size:         1,315,177 bytes
UNP MZ size:             371,579 bytes
UNP load image:          371,467 bytes
UNP overlay/data:        943,598 bytes
Header size:                 112 bytes
Relocations:                  20
Entry CS:IP:            0000:03C1
Initial SS:SP:          5AB1:1C00

This matters. The front of the original file is a real-mode unpacker. After UNP, the visible image contains:

0x00000..0x01bxx  PMODE/VCPI/DPMI/bootstrap strings and transition code
0x01c1c..0x01d09  VGA helper routines: vertical retrace, DAC, mode/tweak setup
0x01d1a..0x01e1x  timer IRQ and keyboard IRQ handlers
0x04a00..0x04e40  file/overlay table loader and simple resource depacker
0x05223..0x05cxx  frontend, sound-system dispatch, GUS driver setup
0x2005d..0x20671  affine/texture/palette-controlled effect loop cluster
0x211b8..0x2161f  text/presentation renderer and screen reveal loops
0x2273c..0x22fxx  heavily unrolled span/warping table builder
0x30000..0x304e9  3D transform, projection, face culling, polygon span filler
0x59c70..0x5a390  planar VGA blit/twister/palette routines

The higher chunks are reached through part-specific dispatch tables and loaded resources rather than through a neat main()-style sequence. The demo is a single executable with internal part scripts and an embedded file system.

Appended Resource Table

The appended overlay ends with an EMF! signature and a compact directory. The first record starts at file offset 0xfb6a9, which is overlay+0xe6302 relative to the packed MZ image. The EMF! signature follows at file offset 0xfb991. Entries are laid out as 32-byte records:

name[16]  file_offset  stored_size  packed_flag  unpacked_size

Examples from the table:

tausta.dat     file 0x000153a7  stored 0x009e92  packed 1  target 0x012c00
anprod.sci     file 0x0001f239  stored 0x000bc8  packed 1  target 0x00fd0a
xwarp.sci      file 0x0001fe01  stored 0x007407  packed 1  target 0x00fd0a
xwarp.scr      file 0x00027208  stored 0x00339b  packed 1  target 0x00918c
doors.pic      file 0x0002b72e  stored 0x0018d5  packed 1  target 0x00fd00
doors.snd      file 0x0002d003  stored 0x01833a  packed 1  target 0x02b90d
cave.dat       file 0x0004533d  stored 0x00589e  packed 1  target 0x010000
ripple.pic     file 0x0004abdb  stored 0x005bff  packed 1  target 0x00fa00
imagine.sci    file 0x000507da  stored 0x002584  packed 1  target 0x00fd0a
endlogo.pic    file 0x00052d5e  stored 0x002f4b  packed 1  target 0x00fd00
ss.dat         file 0x00055ca9  stored 0x01393b  packed 1  target 0x01f400
vscroll.pic    file 0x000695e4  stored 0x000cb5  packed 1  target 0x001680
credits.dat    file 0x0006a299  stored 0x001a9c  packed 1  target 0x005e68
dt.mod         file 0x0006bd35  stored 0x03ac12  packed 0  target 0x03ac12
titlepic.pic   file 0x000a6947  stored 0x00fd00  packed 0  target 0x00fd00
ripple.dat     file 0x000b6647  stored 0x00fa00  packed 0  target 0x00fa00
ripple.pal     file 0x000c6047  stored 0x000300  packed 0  target 0x000300
vertlogo.img   file 0x000c6347  stored 0x002bc0  packed 0  target 0x002bc0
imfont.dat     file 0x000c8f07  stored 0x0036a2  packed 0  target 0x0036a2
twist.dat      file 0x000cc5a9  stored 0x00fa00  packed 0  target 0x00fa00
twist.pal      file 0x000dbfa9  stored 0x000300  packed 0  target 0x000300
twist.pic      file 0x000dc2a9  stored 0x01f400  packed 0  target 0x01f400

The directory explains why the executable itself contains so many filenames even though the archive has no separate part files. The parts ask the internal loader for resource names, and the loader resolves them inside the appended overlay. For the packed .sci resources, the table target is the loader's requested resource target size, not proof that the final on-screen representation is raw chunky pixels. xwarp.sci, for example, begins with RIX3 40 01 c8 00, so the loaded stream still has an image-format header before any part-specific image decode.

Overlay Loader and Depacker

The loader core at 0x4c43 takes a requested filename pointer in EDX, a destination in EDI, and optional offset/length controls in EBX/ECX.

The lookup path:

0x4c43  save registers, store caller offset control at 0x3346
0x4c4a  if table pointer/count 0x3352 is zero, fall back to DOS file path
0x4c75  copy requested ASCIIZ name to scratch at 0x3332
0x4c87  start scanning table at 0x335e
0x4c92  compare first 12 name bytes as three dwords
0x4caf  advance by 0x20 bytes per table entry
0x4cbb  on match, load file handle/segment state from 0x3b5e into 0xd4
0x4cc7  seek to table offset + requested suboffset

When the table entry has packed_flag == 1, execution goes through the resource depacker:

0x4ce0  test entry+0x18 for packed flag
0x4ce6  allocate/prepare a temporary buffer for stored_size bytes
0x4d06  read stored_size bytes from overlay into that buffer
0x4d1d  EBP = stored_size, ESI = temp buffer
0x4d29  call depacker at 0x4e09
0x4d34  release temporary buffer

The depacker at 0x4e09 is a small run/literal stream, not a heavyweight dictionary compressor:

0x4e0d  CL = input_byte & 0x3f
0x4e14  bit 6 says length extends into next byte
0x4e22  count = encoded_count + 1
0x4e25  bit 7 selects run mode
0x4e29  literal mode: rep movsb from packed input to output
0x4e2d  run mode: read one byte, rep stosb to output
0x4e34  loop until bytes consumed reaches stored_size

So the table supports both raw resources (dt.mod, titlepic.pic, twist.pic) and simple RLE-packed resources (cave.dat, doors.snd, ss.dat, many screens/scripts).

PMODE and Machine Setup

The unpacked entry at 0x03c1 is the PMODE bootstrap. It is still 16-bit code using many 32-bit operand/address prefixes.

Important checks and setup:

0x0356  386+ check using FLAGS high bits
0x0377  SMSW protected-mode/V86 check
0x045d  DPMI probe via int 2f, AX=1687h
0x0468  VCPI probe through int 67h path
0x0472  XMS probe via int 2f, AX=4300h
0x048c  A20 fast gate attempt: in/out port 92h, set bit 1
0x04a2  keyboard-controller A20 attempt: out 64h D1h, out 60h DFh
0x04c5  PIT-based fallback/memory stability check using ports 43h/40h

The direct protected-mode entry path:

0x01e7  lgdt [0x0c46]
0x01ec  lidt [0x0c40]
0x01f1  mark TSS descriptor byte at 0x0f09 as 89h
0x01f9  mov eax, cr0
0x01fc  set PE bit
0x01fe  mov cr0, eax
0x0201  far jump to selector 20h, offset 0206h
0x0206  load TR with selector 30h
0x020c  jump to 0x0e7f

The DPMI path is more verbose. Around 0x0702..0x08c7, it allocates selectors, builds descriptors for the code/data spaces, queries free memory with DPMI function 0500h, allocates linear memory with 0501h, and fills per-selector base/limit data. The final far handoff at 0x08dc..0x08eb pushes the prepared selector and offset and uses a 32-bit far return. The surrounding thunks at 0x0e7f..0x112e bridge interrupts and callbacks between PMODE's real-mode and protected-mode sides.

The important practical result is that the demo code can address linear memory directly, including VGA memory at 0xa0000, while still using protected-mode interrupt callbacks for timer, keyboard, DOS file access, and GUS playback.

Timer and Keyboard Interrupts

The timer handler at 0x1d1a is the central scheduler.

0x1d1a  load demo data selector from CS:[0x1e]
0x1d23  send PIC EOI: out 20h, 20h
0x1d27  if byte [0x0b2e] != 1, skip most scheduling
0x1d34  increment word [0x0ae5], the frame/tick counter waited on by parts
0x1d3b  if callback [0x0b48] is nonzero, call it once and clear it

The CRTC start-address scroll/page path is inside the same IRQ:

0x1d54  if byte [0x0b2f] != 1, skip display-start update
0x1d5d  EBX = [0x0b34] + [0x0b38]
0x1d69  wrap to zero if EBX > [0x0b3c]
0x1d73  if unchanged from [0x0b30], skip hardware write
0x1d7b  store new start at [0x0b34]
0x1d81  DX = 3d4h
0x1d85  AL = 0ch, AH = high byte; out AX,DX
0x1d8b  AL = 0dh, AH = low byte; out AX,DX

Then the handler re-arms the timer:

0x1d91  wait for retrace edge through 0x1c1c
0x1d96  out 43h, 34h
0x1d9c  AX = [0x0b2c]
0x1da2  out 40h, low byte
0x1da6  out 40h, high byte

Two callbacks are serviced after reload:

0x1daa  if [0x0b44] != 0, call it once and clear it
0x1dc4  if [0x0b40] != 0, call it as a persistent per-tick callback

The keyboard IRQ at 0x1dd7 is similarly minimal:

0x1de3  AL = in 60h
0x1de5  ignore break codes where bit 7 is set
0x1de9  save make code to [0x0af0]
0x1dee  pulse port 61h bit 7 to acknowledge keyboard controller
0x1df8  out 20h,20h
0x1dfc  if [0x0af0] == 1, enter the shutdown/exit path

This division is important: most effects do not poll the timer directly. They install callbacks or wait for 0x0ae5 to advance, and the IRQ calls their per-frame step.

VGA Register Helpers

The small VGA helpers around 0x1c1c are reused across parts.

Vertical retrace waits:

0x1c1c  DX = 3dah
0x1c20  in al,dx
0x1c22  test bit 3
0x1c24  loop while in vertical retrace

0x1c26  same port
0x1c2a  loop while not in vertical retrace

Black DAC setup:

0x1c30  DX = 3c8h, AL = 0
0x1c36  out DAC index
0x1c37  DX = 3c9h, ECX = 0x300
0x1c40  output zero 768 times

Attribute/overscan helper:

0x1c47  read 3dah to reset attribute flip-flop
0x1c4d  DX = 3c0h
0x1c50  select attribute register 11h
0x1c53  write byte [0x0ae4]
0x1c59  write 20h to re-enable display

Mode/tweak helper:

0x1cc7  DX = 3c4h, AX = 0604h, out AX,DX       ; sequencer memory mode
0x1cd1  read 3dah, then write attr 10h = 61h
0x1ce3  touch CRTC register 07h and force bit 4
0x1cf0  write CRTC 11h
0x1cfa  stream seven dwords through outsl

The higher parts then write directly to 0xa0000 - [0x18], where [0x18] is the PMODE linear-base correction.

Gravis Ultrasound Path

There is no Sound Blaster path. The GUS code starts by parsing ULTRASND from the DOS environment.

At 0x565a:

0x565a  EDI = environment segment from PSP via GS:[PSP+2c]
0x5670  ESI = literal at 0x4dfa, the string "ULTRASND="
0x5675  compare bytes while scanning ASCIIZ environment strings
0x569c  parse base port high digit
0x56a2  base = (digit << 4) + 0x200
0x56b4  parse DMA digit
0x56c8  parse IRQ, including two-digit 10..15 form
0x56f9  save parsed IRQ/control value to 0x52f2

GUS reset/voice register setup then uses the base port in [0x52ea]. The code repeatedly writes a register index to base+0x103, then writes data through base+0x105 or nearby GUS data ports.

The reset sequence at 0x574d:

0x5755  DX = base + 103h, AL = 4ch, out
0x5761  DX += 2, AL = 0, out                 ; reset off/clear
0x576b  dummy reads from base
0x5778  select 4ch again
0x5785  write 1                              ; reset on
0x579b  select 41h, write 0
0x57ac  select 45h, write 0
0x57c3  select 49h, write 0
0x57df  read base+6
0x57ef  select 41h, read
0x5802  select 49h, read
0x5815  select 8fh, read

The voice initialization loop at 0x581c initializes 32 voices:

0x581c  ECX = 0x20
0x5821  decrement voice index
0x5828  out voice select to base+102h
0x5838  write register 00h = 03h
0x5849  write register 0dh = 03h
0x5860  write register 06h = 3fh
0x5877  write register 09h = 0000h
0x588e  loop until all voices configured

Sample upload is at 0x5996. It copies a block descriptor into a local table and streams bytes through base+0x107:

0x599f  pick descriptor slot = [0x5302] * 0x14 + 0x530a
0x59ad  copy block fields from caller descriptor
0x59d4  EBX = remaining byte count
0x59d8  clamp transfer chunk to ECX
0x59de  call source-supply callback through [0x4df6]
0x59e9  set GUS address low through register 43h
0x5a03  set GUS address high through register 44h
0x5a1f  DX = base + 107h
0x5a25  AL = [ESI], out, increment ESI/EDI
0x5a2a  loop for chunk
0x5a32  align next allocation to 32 bytes

Playback of a loaded sample uses 0x5a56, which selects the voice, computes the sample start/end addresses in GUS address units (<< 9), and writes registers 0b/0a/03/02/05/04/00 depending on loop flags. This is a direct GUS hardware driver, not a high-level library call.

Presentation Text Renderer

The presentation part around 0x211b8 and 0x21310 uses imagine.sci and a font buffer to draw block text to mode 13h.

Screen initialization:

0x211c3  AX = saved mode word
0x211c9  EDX = 0xa0000, call PMODE video/mapping helper through [0x30]
0x211d4  EDI = 0xa0000 - [0x18]
0x211df  EAX = 0x0f0f0f0f
0x211e4  ECX = 0x3e80 dwords
0x211e9  rep stosd, clear 320*200 bytes to color 0fh

The actual character renderer is 0x21310:

0x21310  EDI = VGA base
0x2131b  EBX = 0x1f7e8, the 16*20 character-state grid
0x21320  outer row count = 0x10
0x21326  inner column count = 0x14
0x2132d  glyph_index = byte [EBX]
0x2132f  glyph_offset = glyph_index * 0xd0
0x21335  ESI = 0x1f928 + glyph_offset
0x2133d  glyph row count = 0x0d
0x21342  ECX = 4 dwords per glyph row
0x21347  rep movsd, copy 16 bytes of glyph pixels
0x21349  EDI += 0x130, advance to next scanline at 320-byte stride
0x21353  after one character, EDI -= 0x1030 to return to top of next cell
0x2135d  after one text row, EDI += 0x0f00

That is a cell renderer for 20 columns by 16 rows. Each glyph is 16 pixels wide and 13 scanlines high, copied as four dwords per scanline. The state update at 0x212bd increments the character-state bytes until they reach their final glyph numbers, producing the "writer" reveal.

The reveal loop:

0x212bd  clear [0x1f7e3], progress/done counter
0x212c7  ESI = character-state grid
0x212cc  ECX = 16 rows
0x212d1  EBP = 20 columns
0x212d6  AL = current cell state
0x212d8  zero means empty; compare against wave threshold [0x1f7e7]
0x212e8  if AL == 0x0b, count it as complete
0x212f4  otherwise increment the cell
0x21304  increase wave threshold up to 0x10

This is a data-driven writer, not a per-pixel font plotter. Most of the visual cost is bulk rep movsd glyph copies.

Bill Gates Quote and Face Warp

This is the xwarp part: the "640 kb" quote writer and the Bill Gates face mutation/whirlpool credited in the NFO to Saint and The Grim Reaper. The timing below is from the public AVI reference, not from a local DOSBox-X capture.

Verses Bill Gates face-warp GIF from the public AVI reference, showing quote entry, face source, horizontal stretch, vertical squeeze, whirl, and tail fade

The GIF above is assembled directly from the six same-size 512x384 AVI-reference frames listed below. It is a compact view of the face-warp part, not a local emulator capture.

Verses Bill Gates quote and face-warp contact sheet at 01:48 quote start, 01:52 quote plus face, 01:58 source face, 02:02 horizontal stretch, 02:08 squeeze stretch, 02:14 whirl, and 02:20 tail fade

Visible sequence:

01:48.000  quote scene enters from white/black transition
01:52.000  quote is legible over black, with the Bill face part about to enter
01:58.000  face source is visible against a flat grey-violet background
02:02.000  horizontal stretch dominates; features are pulled sideways
02:08.000  vertical squeeze/stretch takes over; head becomes tall and narrow
02:14.000  whirl phase; the face is twisted around its centre
02:20.000  tail/fade phase; the image is rotated/inverted and leaving the part

Verses Bill Gates quote frame at 01:52.000 in the public AVI reference

Verses Bill Gates face source frame at 01:58.000 in the public AVI reference

Verses Bill Gates horizontal stretch frame at 02:02.000 in the public AVI reference

Verses Bill Gates vertical squeeze and stretch frame at 02:08.000 in the public AVI reference

Verses Bill Gates whirl frame at 02:14.000 in the public AVI reference

Verses Bill Gates tail and fade frame at 02:20.000 in the public AVI reference

The resource names line up unusually cleanly:

xwarp.sci  file 0x01fe01  stored 0x007407  packed 1  target 0x00fd0a
xwarp.scr  file 0x027208  stored 0x00339b  packed 1  target 0x00918c

xwarp.sci is not raw 320x200 chunky data immediately after the resource depacker. Its first bytes are:

52 49 58 33 40 01 c8 00 ...
R  I  X  3  width 0x0140  height 0x00c8

So the still image is a RIX-style 320x200 picture resource. The table target of 0x00fd0a fits "picture plus small header", not a naked mode-13h page. xwarp.scr begins with small little-endian dwords:

0a 00 00 00  09 00 00 00  44 00 00 00  62 00 00 00
6a 00 00 00  83 00 00 00  9a 00 00 00  ac 00 00 00

That looks more like a script, offset table, or frame/control stream than image pixels. I would not call the .scr format fully decoded from this pass, but the pair is almost certainly the face picture plus the time-varying warp program for this part.

The visual effect is a 2D image remap, not a 3D polygon scene. The evidence is visible and structural:

A plausible inner loop for the face phase is therefore not "draw Bill as polygons", but "sample Bill through a moving coordinate field":

for each destination row:
    choose row mapping parameters for current frame
    compute starting source u/v or load them from a generated row table
    compute per-column du/dv or load per-column entries
    for each low-resolution output column:
        sx = mapped u, clipped or wrapped into the source picture
        sy = mapped v, clipped or wrapped into the source picture
        color = source[sy * 320 + sx]
        write color twice horizontally to VGA
        advance u/v or advance the map pointer

For the 0x2005d mapper already identified elsewhere in the executable, the inner loop is concrete: it renders 100 rows, 80 iterations per row, two samples per iteration, and duplicates each sample horizontally by copying the byte into both halves of a word before writing to VGA. That exact routine is an affine texture mapper, so I am not claiming it alone proves every xwarp frame. The important point is the same low-resolution sampling strategy: build or update a mapping field once per frame, then run a tight byte-sampling loop over the screen.

The three visible phases correspond to different map generators:

horizontal stretch:
    sx = centre_x + (x - centre_x) * scale_x(y, t)
    sy = y + small_wave(x, y, t)

vertical squeeze/stretch:
    sx = centre_x + (x - centre_x) * scale_x(t)
    sy = centre_y + (y - centre_y) * scale_y(x, t)

whirl:
    dx = x - centre_x
    dy = y - centre_y
    radius = table_or_approx_sqrt(dx*dx + dy*dy)
    angle = atan_table(dx, dy) + twist(radius, t)
    sx = centre_x + cos(angle) * radius
    sy = centre_y + sin(angle) * radius

On a 486 this would be too expensive if computed literally with square roots and transcendentals per pixel. The demo's style points toward tables and fixed-point increments: precompute row bases, sine/cosine or angle tables, and per-frame deltas; then let the inner loop consume those tables with byte loads, adds, and VGA stores. That also explains why the face can move from stretch to whirl smoothly without changing source art. The source image stays the same; the sampling field changes.

Runtime-To-Code Concordance

The public AVI-derived GIF covers only the Bill Gates quote/face-warp passage, so it should be read as a runtime witness for that part rather than as proof of the whole demo. Within that scope, the visible sequence now has a reasonably direct chain from frame to binary structure:

01:48.000  quote part enters
           resources are consistent with xwarp.sci plus xwarp.scr, loaded by
           the overlay table path at 0x4c43 and RLE-depacked at 0x4e09 when
           the packed flag is set

01:52.000  quote is stable on screen
           presentation-style bulk drawing matches the text/grid renderer
           family around 0x211b8/0x21310 rather than BIOS text output

01:58.000  Bill source face is visible
           xwarp.sci begins as a RIX3 320x200 picture; this is the source
           image that the later remap keeps sampling

02:02.000  face becomes horizontally wide
           the effect is a coordinate-field remap: destination pixels keep
           source texture identity while sx changes faster than sy

02:08.000  face is vertically squeezed and stretched
           the same source art is reused with a different per-frame mapping
           field, most likely driven by the xwarp.scr control stream

02:14.000  whirl phase
           this corresponds to a radial/angle remap stage, table-driven in
           practice rather than per-pixel sqrt/atan math

02:20.000  tail/fade leaves the part
           palette streaming/fade code at 0x2022d explains how the part can
           disappear without repainting every source pixel into darker colors

The synchronization backbone for those visual states is the IRQ scheduler, not a loose delay loop. The timer handler at 0x1d1a increments [0x0ae5], services one-shot callbacks through [0x0b48]/[0x0b44], runs a persistent callback through [0x0b40], and can update the CRTC start address from [0x0b34]+[0x0b38]. That gives parts a shared frame/tick clock while still letting a part install its own per-frame stepper.

The visible Bill part also matches the executable's resource and rendering shape. xwarp.sci is a packed picture resource with a RIX3 header and xwarp.scr is a packed control/script-like stream; both are exactly the kind of pair the overlay loader resolves from the internal EMF! directory. After loading, the runtime does not need to keep decoding file formats. It can hold a 320x200 source picture and feed it through a generated sampling map.

The low-resolution mapper at 0x2005d..0x20203 is the clearest recovered code example of that strategy:

0x2005d..0x200d8  rotate/zoom four corner points through trig tables
0x200e5..0x20158  derive fixed-point row and column deltas
0x20183          choose the active VGA page at 0xa0000 - [0x18]
0x2019e          render 100 destination rows
0x201ad..0x20203 sample source rows and duplicate pixels into VGA words
0x2022d..0x202ac stream and mutate the DAC palette for fading

That routine should not be over-identified as "the entire Bill warp" without another control-flow pass, but it proves the important mechanism used by the binary: animated parts are built from fixed-point table generation, compact inner sampling loops, and DAC-side fades. The AVI frames show exactly the kind of continuous texture deformation such a system is designed to produce.

The later polygon material is a separate rendering family. The filled-object engine around 0x30000..0x304d6 builds matrices, transforms vertices, projects them, backface-culls faces, writes scanline edge tables, and fills spans. That explains the NFO's gouraud/polygon credits, but it is not the right mental model for the Bill Gates segment. The face-warp passage is image-space resampling; the 3D cluster is object-space geometry.

Affine Texture Mapper / Ripple-Style Mapper

The routine beginning at 0x2005d builds four transformed corner points and then fills a 160x100 low-resolution field into VGA memory as duplicated pixels. The code uses fixed-point math and row/column deltas.

Corner transform:

0x2005d  ESI = 0, EBP = 4 corners
0x20064  ECX = angle index [0x1c728]
0x2006b  EAX = sin/cos table [0x1c738 + angle*4]
0x20072  imul corner_x
0x2007c  EAX = second trig table [0x1cb38 + angle*4]
0x20083  imul corner_y
0x2008b  combine/subtract terms for rotated x
0x2008d  multiply by zoom [0x1c72a]
0x20095  shrd EDX,EAX,7 for fixed-point rescale
0x20099  add x center [0x1c732]
0x2009f  store transformed x

The y component repeats the same pattern with swapped/additive terms and center [0x1c72e].

Interpolation setup:

0x200e5  divide top-edge delta by 100 rows, store integer/fraction parts
0x20122  divide left-edge delta by 160 columns
0x2015f  load starting source u/v fixed-point pair
0x20183  add source row table base [0x1c220]
0x2018f  EDI = active page start * 4 + 0xa0000 - [0x18]
0x2019e  ECX = 100 destination rows

The inner mapper at 0x201a3:

for each of 100 rows:
    save row u/v and x/y accumulators
    ECX = 80 columns, because each iteration writes two duplicated pixels
    row_base = table[v]

0x201ad  EAX = row table for current v
0x201b4  advance v fraction by column delta
0x201c1  AL = texture[row_base + u]
0x201c4  advance u fraction by column delta
0x201cb  AH = AL
0x201d3  write AX to VGA

0x201d6  repeat a second sample/write
0x20200  EDI += 4
0x20203  loop for 80 iterations

    restore saved accumulators
    advance row edge fixed-point values
    loop for next row

The two samples per loop and AH=AL duplication write four VGA pixels per loop as two 16-bit stores. The visible resolution is 160x100 doubled horizontally, which is a common speed tradeoff for animated affine/warped textures on 486-era VGA.

Palette fade for this cluster is at 0x2022d:

0x20236  ESI = current palette buffer
0x2023c  DX = 3c8h, AL = 0
0x20242  write DAC start index
0x20245  DX = 3c9h
0x2024a  output 3 bytes per color, 255 colors
0x20271  EDX = 63 * fade_step / 64
0x20280  EBX = 64 - fade_step
0x20297  ECX = 0x2fd bytes
0x2029e  AL = source byte
0x2029f  AL = AL * EBX / 64
0x202a5  add EDL bias
0x202a7  store into active palette

This fades by mutating a palette buffer, then streaming it to the DAC every tick, rather than by touching the framebuffer.

3D/Gouraud/Polygon Engine

The 3D cluster around 0x30000 contains a conventional but well-optimized protected-mode software renderer.

Rotation Matrix

The first routine computes a 3x3 matrix from trig tables:

0x3000c  load trig table value from 0x2aa1c[angle]
0x3001c  load paired trig value from 0x2ab84[angle]
0x30025  signed multiply table terms
0x3002d  shrd EDX,EAX,10h to keep fixed-point high result
0x30031  keep partial product in EBX
0x30033  multiply/add second term
0x30053  store matrix coefficient at 0x27184

The coefficients land at 0x2716c..0x2718c.

Vertex Transform

0x300ab applies the matrix to a point array:

for each point:
    x' = m00*x + m01*y + m02*z
    y' = m10*x + m11*y + m12*z
    z' = m20*x + m21*y + m22*z
    ESI += 0x10
    EDI += 0x10

The multiply/add sequence is explicit:

0x300bc  imul [ESI+0] by m00
0x300be  imul [ESI+4] by m01 into EBX
0x300c2  imul [ESI+8] by m02 into ECX
0x300c6  add EBX
0x300c8  add ECX
0x300ca  store transformed x

Projection

0x3011a projects transformed points to screen coordinates:

0x3011a  ECX = z
0x30125  z += [0x2b8d0]
0x30127  z += 0x320000
0x3012d  EBX = 0x180, x scale
0x30132  imul x by scale
0x30134  idiv z
0x30136  add screen center x [0x2b8c8]
0x3013c  store projected x
0x30141  EBX = 0x140, y scale
0x30146  imul y
0x30148  idiv z
0x3014a  add screen center y [0x2b8cc]

This is normal perspective division. The large 0x320000 bias keeps the denominator positive and prevents near-plane singularities for the intended object ranges.

Backface Test

0x3015d computes a signed 2D area for each face:

dx1 = x[v_b] - x[v_a]
dy2 = y[v_c] - y[v_a]
term1 = dx1 * dy2

dy1 = y[v_b] - y[v_a]
dx2 = x[v_c] - x[v_a]
term2 = dy1 * dx2

visible = (term1 - term2) > 0

The result is written as a byte flag at face+1. A negative or zero area marks the face as hidden.

Triangle/Quad Handling

0x301cb walks face records:

0x301cb  EDX = face flags/color
0x301cd  if DH has sign bit set, skip face
0x301d3  store face color to 0x271b4
0x301d9  EBP = vertex count / polygon type
0x301dc  load up to four vertex indices
0x301e8  map indices through projected coordinate pointer table at 0x2b670
0x30204  call polygon setup
0x3020b  advance face record by 0x20

At 0x30212, all vertex y values are biased by 0x03200000 before the edge sort. The polygon is then split into one or two triangles:

0x30240  if EBP == 3, only draw triangle (a,b,c)
0x30245  otherwise draw triangle (a,c,d)
0x30260  then draw triangle (a,b,c)

Edge Table

0x30286 sorts the three y coordinates, stores them at 0x271cc, and turns them into 16.16 fixed-point edge walkers:

0x3028b  sort y0, y1, y2 by exchange
0x3029b  store sorted y values
0x302a4  shift y values left by 16
0x302d1  delta_y long edge = y2 - y0
0x302e1  delta_x long edge = x2 - x0
0x302f5  slope_long = delta_y / delta_x
0x30301  compute upper edge slope
0x3033d  compute lower edge slope

The code chooses which pair of slopes is left/right by comparing them:

0x30374  compare ECX and EDX slopes
0x30378  xchg if needed
0x30383  call 0x30415 to write spans

0x30415 is the edge-table writer:

0x30415  if current left bound is greater, replace it
0x3041b  if current right bound is smaller, replace it
0x30423  advance left by slope
0x30425  advance right by slope
0x30427  advance edge-table pointer by 8
0x3042a  loop for scanline count

Span Filler

0x3042e walks 200 scanlines of generated left/right spans:

0x30443  EDX = 200 rows
0x30448  EBP = framebuffer base table [0x2d134] + 0x61ac0
0x30454  ESI = left x, EDI = right x
0x30459  convert 16.16 x values to integer
0x3045f  clip left/right to 0..319
0x30483  if right >= left, call span writer
0x3048c  reset span to sentinel values
0x30499  EBP += 320 for next scanline

0x304a6 is the optimized horizontal fill:

0x304a6  EBX = right - left + 1
0x304ad  if length < 12, use byte-only fill
0x304b2  load packed color dword from 0x271b4
0x304ba  EDI = framebuffer_row + left
0x304be  compute bytes needed to align EDI to dword
0x304c8  rep stosb for leading unaligned pixels
0x304ca  ECX = remaining / 4
0x304cf  rep stosd for middle
0x304d1  EBX &= 3
0x304d6  rep stosb for trailing pixels

This is not merely a wireframe effect. It has a full filled-polygon pipeline: matrix build, point transform, perspective projection, backface culling, edge building, clipping, and aligned span writes. The NFO's "gouraud routines" credit matches this cluster, although this specific span routine is a flat-color fill; the broader object system likely switches span routines through dispatch tables.

Twister / Planar VGA Copy Cluster

The cluster around 0x59c70 handles an unchained or planar-ish VGA part. It uses direct sequencer/graphics-controller writes plus a ring buffer.

Initial transfer:

0x59c74  DX = 3c4h, AX = 0f02h, enable all plane masks
0x59c7e  EDI = [0x586c8] + 0xa0000 - [0x18] + 7
0x59c93  ESI = current source pointer [0x58724]
0x59c99  EBP = 0x28 rows
0x59c9e  call 0x59d2d

0x59d2d copies a vertical stripe:

0x59d2d  ECX = 16
0x59d32  rep movsb, copy 16 source bytes
0x59d34  wrap source pointer if it reaches end [0x586c4]
0x59d42  EDI += 0x94
0x59d48  loop EBP times

The stride 0x94 is 148 bytes, not the standard 80 or 320. Combined with the part's starting offsets and plane masks, that produces a skewed/twisted copy pattern rather than a linear bitmap blit.

The center bands use specialized nibble/byte selectors chosen by [0x58728]:

0x59d4c  read mode byte
0x59d58  mode 0 -> 0x59db8
0x59d63  mode 1 -> 0x59df4
0x59d6e  mode 2 -> 0x59e2c
0x59d79  mode 3 -> 0x59d80

Each mode copies eight visible bytes per row, but with a different phase:

mode 3: add 2 to source/dest, copy 2, skip 2, copy 2...
mode 0: start one byte later, copy pairs, then back up one byte at row end
mode 1: copy pairs from the current alignment
mode 2: copy 1, skip 2, copy pairs, then copy a final byte

So the part is not just copying a picture. It is changing which plane/phase lands on the screen for each band, creating an interleaved twisting motion.

The vertical line insertion/erase at 0x59e64 uses graphics-controller write mode:

0x59e64  select GC register 5 at 3ceh
0x59e6b  read current value
0x59e6d  OR AH with 1, write mode control back
0x59e72  sequencer map mask all planes
0x59e7c  EDI = active VGA base + 0x2058
0x59eb0  loop 100 times copying one byte every source +0x27, dest +0xa3
0x59ec2  back source by 0x0fa0 and dest by 0x4064
0x59ed3  draw second 100-byte column
0x59f0b  clear write-mode bits again
0x59f20  erase/zero vertical path every 0xa3 bytes

This is classic VGA-plane trickery: a small number of CPU writes are expanded by the VGA latch/plane hardware into visible columns or masks.

Twister Palette Fades

The same part uses a compact 96-color palette. Palette upload:

0x5a220  DX = 3c8h, AL = 0
0x5a226  write DAC start index
0x5a227  DX = 3c9h
0x5a229  EBP = 0x60 colors
0x5a22e  ECX = 3
0x5a233  rep outsb for one RGB triplet
0x5a235  loop 96 colors

Fade-in at 0x5a211:

0x5a253  EBX = 64 - fade_step
0x5a25e  ESI = original palette at 0x58735
0x5a263  EDI = working palette buffer [0x586bc]
0x5a269  ECX = 0x120 bytes
0x5a270  AL = source component
0x5a271  AL = AL * EBX / 64
0x5a277  write component
0x5a27b  fade_step++

Fade-out/invert at 0x5a282 does three interleaved passes over R, G, and B components:

0x5a2c8  EDX = 0x3d * fade_step / 64 for first channel
0x5a2e2  process 96 components with stride 2 in source/dest
0x5a2fc  EDX = 0x3b * fade_step / 64 for second channel
0x5a318  same loop, starting one byte later
0x5a332  EDX = 0x38 * fade_step / 64 for third channel
0x5a352  same loop, starting two bytes later
0x5a36c  fade_step--

That gives each RGB channel a slightly different destination bias (0x3d, 0x3b, 0x38), so the fade is colored rather than a purely gray fade.

End Logo / Bitmap Conversion

The setup around 0x5a0b3 converts chunky data into planar VGA:

0x5a0b3  clear graphics-controller write mode bits
0x5a0c1  AX = 1102h, initial sequencer map mask value
0x5a0c5  EBP = 4 planes
0x5a0ca  out AX to 3c4h
0x5a0d0  save source/dest/count
0x5a0d3  movsb one byte, then source += 3
0x5a0d7  loop over byte count
0x5a0dd  ESI++ to move to next chunky byte phase
0x5a0de  rol AH,1 to rotate the plane mask
0x5a0e0  repeat for four planes

This is a chunky-to-planar splitter. It walks the same chunky source four times, copying every fourth byte into a different plane by rotating the sequencer map mask.

What Verses Is Doing

Verses is a mature 1994 protected-mode PC demo. Its core technical shape is:

The demo's "fancy" feel is not coming from one miracle routine. It is the combination of protected-mode memory freedom, embedded resources, a callback driven runtime, and several small inner loops that are each specialized for one visual job.