Analysis model: gpt-5.5 xhigh

Fyvush by Jamm - Technical Dissection

Scope

This is a static technical dissection of Fyvush by Jamm, released for MS-DOS in the Assembly 1994 PC 64KB intro competition. The Assembly 1994 result file places it third in that competition, after AirFrame by Prime and Space Jam by Fascination.

Public references:

This is not a source-level reconstruction. It is based on the public archive, the included information text, the MZ/RNC executable layout, the RNC-expanded image, and static disassembly of the unpacked runtime.

Offset notation:

Examined Files

Archive contents:

FILE_ID.DIZ          314 bytes
FYVUSH.EXE        49,902 bytes
INPHO.TXT         12,276 bytes
JAMMDIST.LST         668 bytes

Hashes:

0527155653ee573aaa5394464a420b735cae91c39fd0763a381fa79786a75bb7  fyvush.zip
0845ca2be7bee74c8c6abb6f4667074eae26e8a1f81b8f589161cdef8c993e40  fyvush.diz / FILE_ID.DIZ
d6e7b18e561b03fbb0d82de7650bc0939f60362fa8c54a7d3699514c1cd73f72  FYVUSH.EXE
301b287e5eb81bca88008c1a4bc57621b0919fffe26a2fb7c040753243e01e2f  INPHO.TXT
62c170dd084c3bf7c149839f6103fdf237f559f6aa843b9b668d2396436eba83  JAMMDIST.LST
8a475ba80342085f1b5052fecfa200895e43da35e303b69d2e551aff3e441862  RNC-expanded payload

The info text credits:

The same text describes a 386/40 target, a fast compatible VGA card, about 550 KB of conventional memory, and Gravis UltraSound with 256 KB. It also says the intro is fully assembler, that the tune was made with ProTracker for GUS, and that the raster interrupt is used only in the Hellraiser cube. The code confirms the overall shape: this is a tight real-mode VGA/GUS intro, not a protected-mode framework.

Executable And Packing

FYVUSH.EXE is an MZ executable with a tiny loader/depacker tail and one RNC method-1 block beginning immediately after the 32-byte MZ header.

file size:              49,902 bytes
MZ header size:             32 bytes
entry point:         CS:IP 0bea:000c
entry file offset:        0xbecc
relocations:                 0
RNC block offset:          0x20
RNC method:                   1
RNC packed size:        48,772 bytes
RNC unpacked size:     529,296 bytes

The expanded payload starts with deliberate noise: an anti-ripper taunt and a large number of fake M.K. ProTracker signatures. A simple string scan sees thousands of module-looking markers near the front, but that is not the code entry and not a trustworthy module boundary.

The real loader tail at original EXE+0xbec0 contains the far entry and stack values that are applied after decompression:

raw entry:  28a5:1858  -> expanded+0x2a2a8
raw stack:  2885:0200

The RNC method-1 depacker at original EXE+0xbf1a..0xc14b is a compact bitstream/Huffman decoder. Its structure is the normal RNC method-1 shape:

The important practical point is that expanded+0x0 is data/decoy text. The first real top-level code I found is the far target expanded+0x2a2a8.

Startup Path

Startup is plain DOS/real-mode setup. At expanded+0x2a2a8 it saves the initial ES, switches DS to segment 09e8, and asks DOS to resize the program memory block:

AH=4Ah, BX=83d6h, DX=1943h, INT 21h

Then it runs a small machine/environment check sequence. One check uses smsw ax; if protected/v86 mode appears to be active, it prints a warning and lets the user abort. It sets GUS defaults in the player segment:

GS = 3fe6
GS:[0f88] = 0036h
GS:[0f70] = 0220h

The ULTRASND= parser is at expanded+0x2a17a. On success it prints the GUS port message and later calls the resident player code through far calls in segment 3fe6, for example 3fe6:2360, 3fe6:2383, 3fe6:24de, 3fe6:2258, 3fe6:22fe, and 3fe6:24d1.

Before the scene chain starts, it blanks the VGA framebuffer, starts the music path, and masks the interrupt controllers:

save PIC masks from ports a1h and 21h
out 21h, feh
out a1h, ffh

The fixed top-level scene order is:

expanded+0x2ac89  first logo / wave / quad transition
expanded+0x2b13d  point and line table scene
expanded+0x2f490  Hellraiser cube / raster section
expanded+0x2b43f  plane-separated texture columns
expanded+0x2e053  rotozoom / texture slab and wave lines
expanded+0x3186f  3D object / vector scene
expanded+0x2f617  timer-driven textured polygon scene
expanded+0x2e626  line wipe plus second rotozoom phase
expanded+0x2eaec  vertical wave/slice scene
expanded+0x2f15c  final text and fade

After the last scene it restores the saved PIC masks, stops/restores the GUS player, switches to BIOS text mode 3, prints the exit text, and terminates.

Shared VGA Helpers

Several shared routines are reached through wrapped 16-bit calls. The useful physical locations are:

expanded+0x3210f  fill A000 with ECX dwords of EAX, starting at DI
expanded+0x32124  set CRTC display start from BX via registers 0Ch/0Dh
expanded+0x32136  set sequencer map mask plus graphics-controller state
expanded+0x3215a  select VGA sequencer map-mask register
expanded+0x32165  clipped Bresenham line draw in planar memory
expanded+0x322dc  draw four connected edges from points at 4e2a..4e38
expanded+0x3234f  set one DAC color from stack arguments
expanded+0x32368  wait for vertical retrace start
expanded+0x3237a  wait for display-enable edge using 3DAh bit 0
expanded+0x3238c  BIOS mode 13h plus unchained/tweaked VGA setup
expanded+0x3240b  blank all 256 DAC entries

expanded+0x32124 is the page flip. It writes high and low bytes of the start address to CRTC registers 0Ch and 0Dh. The scenes flip by toggling offsets such as 0x4b00, 0x2580, or 0x3e80, depending on the mode and buffer height.

The clipped line drawer at expanded+0x32165 is a normal integer Bresenham, but adapted for unchained VGA:

  1. Reject the segment if both endpoints are outside the clip rectangle.
  2. Compute signed dx, dy, major/minor axis, step signs, and error terms.
  3. Choose ES as a000h + (page_offset >> 4).
  4. For each point, set the sequencer map mask from x & 3.
  5. Store one byte at y * 0x50 + (x >> 2).
  6. Advance the major axis and conditionally step the minor axis from the Bresenham error accumulator.

The polygon/span helpers are more interesting because they exploit VGA write masks:

This common raster layer explains why many visual parts look different but share the same state fields: 39a0..39a6 are clip bounds, 4e04/4e26 describe scanline start/count, 4e44 is page offset, 4e47 is draw color, and 4e2a.. holds projected points.

Opening Scene: Logo, Wave Columns, Rotating Quad

The first scene starts at expanded+0x2ac89. It sets DS to 09e8, initializes clip bounds, enters BIOS mode 13h, applies the tweaked VGA setup, clears A000, and blanks the DAC.

The early loop is a timed reveal:

The first genuinely hot renderer is expanded+0x2a773. It is a column wave or tunnel-like copier:

DS = 09e8
ES = a3ea
GS = 6097
phase = ds:0840
source table = gs:[06ca + index]
destination = es:[di + row offsets]

The core math uses a sine/motion table at ds:0740, multiplies by 0xc8, and uses shrd ...,9 to keep a fixed-point scale. The inner loop advances a fixed-point source coordinate, takes the high byte as an index, clamps it against a rough -100..200 range, and writes word pairs into a work segment. It is unrolled enough that each step writes two adjacent pixels while the source index slides through the table.

After that, the scene writes a small bordered/wipe rectangle directly in A000. It walks di by one 320-byte scanline and writes paired words around di+0x3b75, producing a rectangular transition without invoking the general polygon engine.

The last part is a four-point rotating projection:

expanded+0x2ae8a clears the current page, calls the projector, draws through the shared polygon/line path, flips with expanded+0x32124, and toggles the page offset with ds:4e44 ^= 0x3e80.

Point And Line Table Scene

The second scene starts at expanded+0x2b13d. It uses the attribute controller and clears the full planar framebuffer. Before drawing, it builds lookup tables:

The generator at expanded+0x2b034 writes 0x6f x/y pairs to 0x39b0. It uses several phase counters (0xbe8, 0xbea, 0xbec, 0xbee) plus an amplitude at 0xbf0. The generated point strip is handed to the common polygon/line machinery by setting fields such as 4e04, 4e26, and 4e47, then calling the scanline/draw helper.

Conceptually this scene is not a texture effect. It is a table-driven vector oscillator: compute many y/x positions from phase-shifted tables, draw connected spans/lines, flip page, advance phases, repeat.

Hellraiser Cube And Raster Timing

The Hellraiser cube scene begins at expanded+0x2f490. This is the one part the info text calls out as raster-interrupt-related. There are two different timing mechanisms:

  1. A polling raster routine used in the cube frames.
  2. A later PIT IRQ0 hook used by the following scene.

The cube's per-frame raster routine is expanded+0x2f3fa. It is synchronized by polling VGA status, not by a permanent ISR:

DS = ES = 3f56
for cx = 0..0x15d:
    wait for 3DAh bit 0 active
    value = table[si++]
    wait for 3DAh bit 0 inactive
    out 3D4h, AX where AL=13h and AH=value

CRTC register 13h is the logical screen offset/pitch register. Changing it per scanline bends the displayed memory layout. The visual result is the "Hellraiser" deformation: the cube/table data is ordinary memory, but the VGA scanout is being steered line by line.

The scene masks interrupts around the timed section with cli/sti, calls the GUS player periodically, and decrements a frame countdown. It is highly ET4000-era VGA friendly: tight polling of 3dah and immediate CRTC writes assume a responsive VGA implementation.

The separate timer hook is installed by expanded+0x2f5b7. Because the active CS base is expanded+0x28a50, the installed vector cs:6b50 physically lands at expanded+0x2f5a0. The ISR body is tiny:

push ax
out 20h, 20h              ; EOI to master PIC
inc word ptr cs:6b65      ; tick accumulator
call 3fe6:1c07            ; music/update callback
pop ax
iret

The hook programs PIT channel 0 mode 3 with divisor 0x4dae, saves the old INT 08h vector at cs:6b61, and restores it in expanded+0x2f5e1.

Plane-Separated Texture Columns

The fourth scene starts at expanded+0x2b43f. Its hot loop at expanded+0x2b582..0x2b77e is a vertical column renderer tuned for planar VGA.

The outer loop selects a VGA plane:

out 3c4h, index 2
out 3c5h, plane_mask

Then it computes di = page_offset + 0x0976 and walks columns. The source coordinate is a combination of:

The inner column is unrolled as repeated source samples and vertical stores:

sample = gs:[computed_texture_index]
es:[di + 000h] = sample
es:[di + 050h] = sample
es:[di + 0a0h] = sample
...
es:[di + 5a0h] = sample

The pitch is 0x50 bytes because unchained 320-wide planar VGA stores one byte per four horizontal pixels per plane. Rendering one plane at a time lets the code use straight byte stores after the map mask is selected. The cost is four passes, but the inner loop stays branch-light and aligned to the memory layout.

Rotozoom And Wave-Line Scene

expanded+0x2e053 is a larger scene built around the texture mapper at expanded+0x2e3d8. It prepares coordinates, palettes, and line tables, then uses a two-pass planar mapper.

The mapper's setup:

The core texture sample uses high bytes of two fixed-point accumulators:

bl = high(u)
bh = high(v)
pixel = gs:[bx]

Then it writes two pixels as a word. It first draws planes 0/1 with map mask 0302h, then planes 2/3 with map mask 0c02h. The row loop is unrolled four word writes at a time and uses a logical pitch of 0x50, matching the same Mode X memory geometry as the column renderer.

The palette side is also active:

The companion line generator is expanded+0x2e5e0. For nine phase offsets it fills 100 words at 0x39b0 from a base line table and a wave table at 0x1a40, then calls the shared scanline/line function. This is why the scene combines a texture-slab look with overlaid moving line/wave geometry.

3D Object Scene

The call printed as 0x2186f is physically expanded+0x3186f. It is a compact 3D object/vector part using the same shared polygon layer.

Setup:

The object transform in this region uses a 3x3 fixed-point matrix. Routines around expanded+0x307fe..0x30aa6 load sine values through GS, build matrix terms with signed 32-bit multiplies and sar 9/sar 18 scaling, transform eight 3D points, and project them with:

screen_x = center_x + ((x << 8) / z)
screen_y = center_y - ((y << 8) / z)

Projected points are stored around 0x037e/0x0380. The renderer then assembles quads from an index table, determines face orientation with a 2D cross product, selects an FS texture/pattern segment for visible faces, and draws through the span/line helpers.

There is also a self-modifying patch near the end of the scene: it writes NOPs over bytes around the current code segment before switching to another VGA setup. In context it looks like a timed transition patch rather than an obfuscation layer.

Timer-Driven Textured Polygon Scene

expanded+0x2f617 uses the PIT hook described earlier. The main loop drains the accumulated timer count:

xchg cx, cs:6b65
while cx != 0:
    call expanded+0x2f86f   ; advance three phase counters
    ds:03e7++
    if ds:03e7 >= 0x03c0:
        ds:00ae += 2

It then updates the 3D transform, assembles six quads from projected points, rejects backfacing polygons using the signed cross product at expanded+0x2f798, updates a 0x48-byte DAC palette from face/brightness data, and calls the texture mapper at expanded+0x2f893.

expanded+0x2f893 is one of the classic inner loops in this intro. It computes vertical edge interpolation between polygon endpoints, derives affine texture increments, selects the VGA plane from x & 3, and writes a heavily unrolled column:

advance u/v fixed point
texture_byte = fs:[texture_index]
es:[di + 0000h] = texture_byte
es:[di + 0050h] = texture_byte
es:[di + 00a0h] = texture_byte
...
es:[di + 3e30h] = texture_byte

The repeated +0x50 stride is again the 320-pixel planar pitch. The code uses 32-bit registers in real mode for affine accumulators, rotates words to expose fraction/integer halves, and relies on unrolled stores rather than a tiny inner loop. The result is a fast planar textured face renderer that spends most of its cycles doing predictable adds, ADCs, one texture read, and one byte write.

The auxiliary motion routines at expanded+0x30511, 0x30591, 0x30611, and 0x306aa interpolate endpoints and table indices. They reset counters, compute fixed-point deltas with signed division, and then let the frame loop add those deltas until the next segment is reached.

Line Wipe And Second Rotozoom Phase

expanded+0x2e626 first clears/sets up the screen, waits about 60 retraces, then uses the line engine to draw a simple triangular/line structure. It copies the generated points through buffers at 0x1950 and 0x1860, then drives two line waves using expanded+0x2e5e0.

The loop has a wipe behavior:

After the wipe it switches back into the texture/palette machinery from the earlier rotozoom scene, reusing 0x2e55f, 0x2e592, 0x2e5a6, 0x2e5c4, 0x2e360, and 0x2e3d8. This part is less a separate engine and more a second scripted pass through the same texture slab core with new palette timing.

Vertical Wave/Slice Scene

expanded+0x2eaec starts by setting a mode 0Dh-derived planar setup at expanded+0x2e874, copies a 0x640-byte source block from segment 3a42 to 3aa6, and builds two scaling tables:

The per-frame renderer is expanded+0x2e991. It fills 0xf0 x-pairs in 0x39b0. Each pair is the sum of two table-driven waves, shifted down by ten bits, biased by the current plane offset, and then separated into left and right x coordinates. After all scanlines are prepared it calls the planar span fill helper with color/state 4e46/4e47 = 2.

There is a small raster color trick at the end of the draw call:

wait for 3DAh bit 0 edge
write attribute-controller index 33h
write low three bits of the current phase

That means part of the movement is not in framebuffer memory at all; the attribute controller is being nudged in sync with display timing, changing how the already-written pixels are interpreted.

The palette transition at expanded+0x2ea73 blends 12 DAC bytes between two small palettes. It uses old * (256-t) >> 8 plus new * t >> 8, increments the blend counter to 0x100, and later advances a separate fade counter once the scene reaches its exit threshold.

Final Scene And Text Fade

The final sequence starts at expanded+0x2f15c. It sets up text/glyph state, calls expanded+0x2ecde to build a 0x40-entry sine-derived table, and enters a page-flipped loop that updates music, draws, flips, and polls keyboard port 60h for Escape.

The bitmapped text renderer is expanded+0x2eca0. It is an 8x16 glyph blitter using the VGA graphics-controller bit mask:

  1. Select the target VGA plane/mode through expanded+0x32136.
  2. Select graphics-controller register 8, the bit mask.
  3. Load the glyph row from the font pointer at ds:2ef8.
  4. Shift it by the x bit offset.
  5. Output the low and high mask bytes to 3cfh.
  6. Touch es:[di+1] and es:[di] with xchg to trigger masked writes.
  7. Advance by 0x28 bytes per glyph row.

The main reveal iterates 0x1e glyph positions from 0x31e0, computes byte and bit offsets, plots the glyph on both visible and hidden pages, and then flips.

Fade-out is done from the real DAC:

This is a good example of the intro's style: the visual state is often a mix of framebuffer data, VGA plane state, DAC state, and timing-side effects rather than one clean chunky framebuffer.

Technical Character

Fyvush is a 64K intro built around small, specialized real-mode kernels:

The code is dense but internally consistent. Once the RNC decoys and 16-bit call-wrap addresses are accounted for, the intro resolves into a handful of well-reused primitives: page flip, retrace wait, palette upload/blend, line scan conversion, planar span fill, planar texture columns, and table-driven motion.