Analysis model: gpt-5.5 xhigh

The Easy Way Out by The KIP Brigade - Technical Dissection

Scope

The Easy Way Out is a 1995 MS-DOS/GUS demo by The KIP Brigade, also written as KIP Brigade or TKB in the release material. Public party listings place it third in the Somewhere In Holland 1995 PC demo competition, behind Goldorak by Real-Time and Model 0 by Analogue.

Public references:

This writeup is based on both scene.org archives, the bundled text file, a short DOSBox-X smoke run, UNP expansion of the PKLITE executable, and static disassembly of the expanded EASY.EXE. It is not a source-level rebuild.

Old private contact material from the bundled text is deliberately omitted.

Credits And Release Notes

The bundled EASY.TXT identifies the demo as the third-place SIH 1995 entry. It credits the 3D engine to Sharp, the glenz part to LED, the music replay to Mikmak from Hardcode and Sharp, the music to PoZoR, graphics to Comik, aNARKY, and Mr.M, 3D objects to PoZoR and Rtf, and storyboards to aNARKY and PoZoR.

The text also explains the slightly confusing release state. The party version skipped broken parts; the later fixed archive enables those parts. The music path is GUS-only, and the release notes recommend a 486DX2-66, a 512 KB GUS, and an ET4000 VGA card. The code fits that target: it is a real-mode Borland-ish segmented executable with hand-tuned VGA and 32-bit fixed-point math, but not a protected-mode engine.

Examined Files

Archives:

714266f2c5ffa55ccb889d07941f39f46d44df1ff836e74c9cf436b9a7eb7ad1  easy.zip
b1164f4ac2eea84ff1f5b0d27aa404b917ee9fef90b2005a9cc1cf9e6624ab54  tewo_fix.zip

easy.zip contains:

EASY.EXE       336873 bytes  1995-07-21 09:41
EASY.TXT         2677 bytes  1995-07-21 09:41
PLAY.DAT       444109 bytes  1995-07-21 09:41
FILE_ID.DIZ       500 bytes  1995-10-23 11:24

tewo_fix.zip contains the same EASY.EXE, EASY.TXT, and PLAY.DAT, a different FILE_ID.DIZ, and an extra WORST.COM:

41b395372c002697d977d7775dcb57998e0af9f6b1102653af84187528bcb86a  EASY.EXE
f1dab78b62fcd7ce8c1afe1e06c47686b4ab3ee421cb165a61fb4c0368927f6c  EASY.TXT
6634462b42bfdec8ed303c13cfb413ab2b6c7c4d49a38624ccd74eee692253af  PLAY.DAT
ba25bb40b4a96d2d145fca9b8b9b66b6bc2d4639877cc851b3ced59a73c27bda  WORST.COM

EASY.EXE is PKLITE-packed in the archive. DOS UNP 4.11 expands it to a valid MZ image while preserving the appended payload:

expanded EASY.EXE size: 387100 bytes
expanded EASY.EXE sha256: 758970273a7b26cdbd5f31fefcb7262e0a287dad2629db9bf857a04c247431e2

The expanded header:

MZ pages: 0x00b5
relocations: 0x032d
header paragraphs: 0x00cd
initial CS:IP: 0000:0025
initial SS:SP: 21c4:fff0
declared image size: 92480 bytes
header bytes: 3280
entry file offset: 0x0cf5

The important point is the mismatch between the declared MZ image and the physical file length. The unpacked code image ends around 0x16940; the file continues to 0x5e81c. That appended area is not accidental slack: it is the embedded TEWO.DAT multi-file package.

Top-Level Runtime

The entry at file 0x0cf5 is a compact script of far calls:

0cf5  call 10db:0000       ; runtime startup
0cfa  call 1079:000d       ; text/CRT init
0cff  call 0ac5:0000       ; library init
0d07  call 106a:0000       ; memory check
0d2a  call 0984:07c5       ; sound / PLAY.DAT setup
0d33  call 0a16:002b       ; timed setup variant
0d87  call 1060:0000       ; keyboard IRQ hook
0d91  call 0984:0000       ; music start
0d96  int 10h, ax=0013h    ; 320x200x256
0d9b  call 093d:02aa       ; first picture/loader path
0da0  call 049f:0aa6       ; scene part
0daf  call 049f:1210       ; scene part
0db4  call 0904:01b1       ; scene part
0dbe  call 03da:000a       ; still part
0dc3  call 03df:0ace       ; 3D / glenz part
0dcd  call 02ce:0dd1       ; scene part
0dd7  call 0174:14ff       ; scene part
0de1  call 0119:021b       ; scene part
0deb  call 009a:04ad       ; scene part
0df5  call 0114:000a       ; final part
0dff  int 10h, ax=0003h    ; text mode
0e04  call 1060:007f       ; keyboard IRQ restore
0e19  call 10db:0116       ; exit

The scene chain is strongly timed. Helper 0a16:019c returns a clock value used by scenes for wait thresholds such as 0x07d0, 0x0bb8, and 0x0c1c. Helper 0a16:01ab appears between parts and resets the local time base.

The runtime installs its own keyboard interrupt at vector 09h. The handler at file 0x112d2 reads port 0x60, maintains a 128-byte key state table, records the last scancode, stores ESC as an exit flag, and acknowledges the PIC with out 20h,20h. The matching restore routine writes back the saved vector.

The Two Data Stores

There are two separate data systems:

  1. PLAY.DAT is the music file. It starts with UN04 and contains strings for KIP Trekker v3.06 and MikMod/GUS use. It does not contain the picture names.
  2. The appended area of EASY.EXE is an embedded multi-file package named TEWO.DAT in the executable strings. It contains PIC*.DAT and SCN*.DAT.

The package table begins at file 0x16940 in the UNP-expanded executable:

0x16940  "Multi Data File"
0x1694f  18 entries
0x16951  first entry
0x16acb  first payload byte

Each entry is:

u8       filename length, always 0x0c here
char[12] padded filename
u32le    payload offset from 0x16acb
u32le    payload size

Recovered table:

Entry Offset Size Absolute file offset
PIC1.DAT 0x00000 0x0d5e5 0x16acb
PIC2.DAT 0x0d5e5 0x097db 0x240b0
PIC3.DAT 0x16dc0 0x0505d 0x2d88b
PIC4.DAT 0x1be1d 0x040b4 0x328e8
PIC5.DAT 0x1fed1 0x05c52 0x3699c
PIC6.DAT 0x25b23 0x04ae5 0x3c5ee
PIC7.DAT 0x2a608 0x0204d 0x410d3
PIC8.DAT 0x2c655 0x022e7 0x43120
PIC9.DAT 0x2e93c 0x01475 0x45407
PIC10.DAT 0x2fdb1 0x03e17 0x4687c
PIC11.DAT 0x33bc8 0x06e47 0x4a693
PIC12.DAT 0x3aa0f 0x0313d 0x514da
PIC13.DAT 0x3db4c 0x02092 0x54617
PIC14.DAT 0x3fbde 0x030d9 0x566a9
PIC15.DAT 0x42cb7 0x013fb 0x59782
SCN1.DAT 0x440b2 0x008b2 0x5ab7d
SCN2.DAT 0x44964 0x015b4 0x5b42f
SCN3.DAT 0x45f18 0x01e32 0x5c9e3

The final listed byte ends at 0x5e815, leaving seven trailing bytes before the physical end of the expanded executable.

The package API is concentrated in segment 1027:

The scene code calls those helpers as if TEWO.DAT were an external archive, but the table and payload live inside the executable overlay.

VGA And Timing Helpers

Several small routines are reused throughout the demo:

The code allocates several 0xfa00 blocks. That is 64000 bytes, exactly the visible 320x200 mode 13h screen. Scene parts render into a work segment, copy or blend it to another segment, and finally copy into A000h.

Mode 12h Planar Picture Path

One art path switches to VGA mode 12h and writes planar pixels through the sequencer map mask. The clear helper at file 0xac51 sets ES=A000h, writes sequencer index 2 with mask 0x0f, and clears 0xfa00 bytes.

The rectangle helpers use 0x50 bytes per scanline, matching planar 640-pixel VGA memory (640 / 8 = 80 = 0x50). For each plane, they set port 0x3c4:

mov al,02h          ; sequencer map-mask index
mov ah,1 << plane   ; one writable plane
out dx,ax           ; dx = 03c4h

The picture decompressor at file 0xa6c5 reads a small header and then expands a byte stream to A000h. The inner decode is:

read token
count = 1
if (token & 0xc0) == 0xc0:
    count = token ^ 0xc0
    token = read next byte

repeat count times:
    A000:[plane_offset] = token
    plane_byte += 1

    if plane_byte passes picture width:
        plane = plane + 1
        if plane == 4:
            row = row + 1
            plane = 0
        plane_offset = row * 0x50
        map_mask = 1 << plane
        out 0x3c4, 0x0200 | map_mask
        plane_byte = 0

That is why the PIC*.DAT payloads are much smaller than raw planar screens. They carry run-coded bytes and a coordinate header, then the draw loop walks across VGA planes itself.

Mode 13h Picture Path

The 320x200 path starts at file 0x10423. It opens a named picture, checks the header, reads a 768-byte palette, then expands compressed pixel data into a 0xfa00 byte output segment.

The important decoder is at file 0x10500:

10500  load token from compressed buffer
10516  al = token
1051a  al &= 0c0h
1051c  cmp al,0c0h
1051e  jne literal
10520  count = token & 3fh
1052c  token = next byte
literal:
10541  cx = count
10545  bx = output_index
10549  al = token
1054d  es:[bx] = al
10550  bx++
10555  loop 10545
10557  until output_index >= 0xfa00

After the pixel buffer is complete, 0x10366 converts the palette from 8-bit-ish RGB to VGA DAC range by shifting every component right by two. Then 0x103da loops over all 256 entries and writes index/R/G/B through ports 0x3c8/0x3c9.

This is a plain, fast 320x200 RLE picture loader. The stills are not decoded to the screen directly; they are decoded to a work segment, then copied or blended through shared helpers.

Palette And Image Blend Loop

The blend routine at file 0xa4b0 is a small but useful example of the visual style. It blends a rectangular area from one image buffer to another by phase:

for y = 0..29:
    for x = 0..299:
        old = ds:[si]
        new = gs:[si]
        out = old + ((new - old) * phase >> 6)
        es:[di] = out
        si++
        di++
    di += 20

The loop blends 300 bytes and skips 20 bytes per row, so it targets a 300-wide window inside a 320-byte scanline. The phase scale is six fractional bits, matching the common 0..64 fade convention used elsewhere in the code.

Palette fades use the same integer scaling idea. The routine at file 0xa510 counts a fade level from 0x20 down to zero and, for each palette entry in the active range, multiplies the stored R/G/B components by the level and shifts right by five before writing the DAC.

3D Object Loader

The 3D loader starts around file 0x9228. It opens a scene member, reads count fields, then copies blocks into internal arrays:

The code keeps cumulative counts at variables around 0x576c, 0x7cee, and 0x92d0, then appends newly loaded scene data after the existing arrays. That lets several SCN*.DAT members share one renderer.

3D Transform And Projection

The renderer setup at 846:0079 prepares 256 angle slots. Later, the transform routine at file 0x94b7 uses three angle bytes. It indexes both the sine slot and the slot plus 0xc0, using the +0xc0 phase as cosine.

The math is 32-bit fixed point inside a 16-bit executable. A repeated multiply pattern looks like this:

mov  eax, value
mov  ebx, coefficient
imul ebx
shr  eax,10h
shl  edx,10h
add  eax,edx

That reconstructs a signed 16.16 product from the high and low halves of the 386 multiply. The routine forms the rotation matrix terms, applies translation, stores transformed x/y/z in 12-byte records, then projects:

screen_x = center_x + transformed_x / (transformed_z >> 8)
screen_y = center_y + transformed_y / (transformed_z >> 8)

The projected 2D coordinates are written to the arrays used by the face code.

Face Culling And Sort

The face test at file 0x9130 computes a 2D cross product from three projected points:

dx1 = x0 - x1
dy1 = y0 - y1
dx2 = x2 - x1
dy2 = y2 - y1
visible = dx1 * dy2 < dy1 * dx2

If the sign says the face is turned away, the caller skips it. Otherwise the caller computes a depth value from the transformed z values of the face's three vertices and stores a face index plus depth into two parallel arrays.

The sorter at file 0x39b0 is a quicksort-style partition:

pivot = depth[(left + right) / 2]
i = left
j = right
while i <= j:
    while depth[i] < pivot: i++
    while depth[j] > pivot: j--
    if i <= j:
        swap depth[i], depth[j]
        swap face_index[i], face_index[j]
        i++
        j--
recurse left..j
recurse i..right

The implementation swaps both arrays in lockstep, so the painter's order is kept without moving the face records themselves.

Triangle Edge Builder

Filled quads are split into four triangle calls in the wrapper around file 0x70a4. It clips the horizontal extent to 0..319, calls the edge builder for each edge, then calls the column filler.

The edge builder at file 0x8bac stores per-column top and bottom state in code-segment arrays. For a sloped edge it sorts endpoints by x, computes 16.16 increments, clips negative x, then writes one entry per x:

dx = x2 - x1 + 1
dy_step = ((y2 - y1 + bias) << 16) / dx
u_step  = ((u2 - u1 + bias) << 16) / dx
v_step  = ((v2 - v1 + bias) << 16) / dx

if x1 < 0:
    y += -x1 * dy_step
    u += -x1 * u_step
    v += -x1 * v_step
    x1 = 0

for x = x1..min(x2,319):
    edge_y[x] = y >> 16
    edge_u[x] = u
    edge_v[x] = v
    y += dy_step
    u += u_step
    v += v_step

Vertical edges are handled as a special case: if the x coordinates match, the routine stores only the min/max y and the two texture endpoints for that one column.

Self-Patching Textured Column Filler

The most interesting inner loop is the textured vertical filler at file 0x8de1, with a near-duplicate at 0x8f83. It draws one x column at a time, from the top edge to the bottom edge, sampling an 8-bit texture and skipping transparent zero texels.

Before the pixel loop, setup writes the active values into operands inside the code segment:

That is why a raw disassembler prints instructions that appear to use zero immediates. Those immediates are patched just before the loop runs.

The effective inner loop is:

di = x + y * 320
while y <= y_end:
    tex_y = u >> 16
    tex_x = v >> 16
    texel = texture[tex_y * 320 + tex_x]
    if texel != 0:
        screen[di] = texel
    u += du
    v += dv
    di += 320
    y += 1

The actual instruction sequence in the hot path does this:

mov  eax, patched_u
shr  eax,10h
shl  ax,6
mov  bx,ax
shl  ax,2
add  bx,ax          ; tex_y * 320

mov  eax, patched_v
shr  eax,10h
add  bx,ax          ; + tex_x

mov  al,gs:[bx]
and  al,al
je   skip
mov  es:[di],al
skip:
add  patched_u,du
add  patched_v,dv
add  di,320
inc  y
cmp  y,patched_end
jle  loop

This is a 486-friendly compromise. It is not a horizontal span mapper; it is a column mapper, so the screen pointer advances by 320 each pixel. The tex_y * 320 multiply is built from shifts (*64 + *256) instead of imul, and zero texels are transparent. The self-patched operands avoid extra indexed loads in the inner loop.

Glenz And Moving Text Parts

The glenz/object part around file 0x558e uses the same 3D infrastructure but with simple coordinate blocks initialized around 0x5714. It creates a small set of 12-byte points, copies and offsets them into several objects, and then animates their positions through the same fixed-point interpolation helpers.

The text overlay helper at file 0x56c0 draws a compact 6-by-23 glyph block with transparency:

for row = 0..22:
    for pair = 0..5:
        lo = [si]
        if lo != 0: es:[di] = lo
        si++; di++
        hi = [si]
        if hi != 0: es:[di] = hi
        si++; di++
    di = line_start + 320

The text data near 0x6c8b is byte art rather than code. The renderer treats zero as transparent and writes only nonzero glyph pixels over the current screen.

Sound Path

The executable contains MikMod GUS Driver v1.0 strings, and the release notes state that the demo has no Sound Blaster support. The first runtime branch opens and initializes PLAY.DAT; if initialization fails, the entry script prints an error and exits. If it succeeds, the scene chain calls the music/timer helpers between visual parts.

PLAY.DAT is not a FastTracker module in the normal sense. Its first bytes are UN04, and strings inside it identify KIP Trekker v3.06 and the music title material. The release text says the music was moved through FT2 as part of the production workflow, but the shipped player path is the custom GUS path.

What The Demo Is Doing

The fixed release is a compact trackmo built from a few reusable systems:

The code is not as raw as a tiny intro and not as framework-heavy as a later protected-mode DOS demo. Its personality is in the middle: Pascal/C-like scene structure and archive access wrapped around carefully written real-mode VGA inner loops. The column texture mapper is the standout loop because it spends bytes and complexity on exactly the right places for a 486: precompute per-edge state, patch constants into the hot loop, sample with shifts instead of a multiply, and skip transparent texels with a single branch.