Skip to content

Latest commit

 

History

History
274 lines (231 loc) · 11.6 KB

File metadata and controls

274 lines (231 loc) · 11.6 KB

The 3D engine

The engine is not inside the game. It is three AmigaDOS shared libraries in libs/, each with its own version, each signed by the same name, and all three dated 1992 — a year before the CD32 existed.

libs/math.library     3,724 B   'MATH 1.2 (C) Wyvern 1992'
libs/tridee.library   7,232 B   'TRIDEE 1.3 (C) Wyvern 1992'
libs/vector.library  11,236 B   'VECTOR 1.5 (C) Wyvern 1992'

Wyvern is the second of the two version numbers in the game's own $VER: string (Ratt V2.02 : Wyvern V2.00c), so the split the disc advertises in its title screen is the split on its file system: one author's game, another author's engine, versioned separately.

All three are ordinary Amiga libraries with a resident tag, an autoinit structure and a function table. tools/libinfo.py reads the tables:

Library version data size exports
math.library 1 44 15
tridee.library 1 44 7
vector.library 1 44 4

They are also layered: tridee.library opens math.library in its own init code (bNumath.library sits at offset 0x1A1 of its single code hunk), and the game opens all three itself.

math.library — the fixed-point core

Fifteen exports at LVOs −30 to −114. The game calls two of them, and both are worth reading in full because they are the whole of its arithmetic.

LVO −30, a 2D rotation. The 512-entry sine table lives at hunk offset 0x9E4 and runs to 0xDE3, which is the library's declared end — so it is the last thing in the file, 1,024 bytes, and reading it is free:

0001F2  move.w  $8(a7),d0         ; the angle, off the stack
0001F6  beq.b   $246              ; angle 0: nothing to do
0001FA  andi.w  #$1ff,d0          ; 512 entries = one full turn
0001FE  add.w   d0,d0
000200  lea     $9e4(pc),a1
000204  move.w  (a1,d0.w),d3      ; d3 = sin(angle)
000208  addi.w  #$100,d0          ; + 128 entries = a quarter turn
00020C  andi.w  #$3fe,d0
000210  move.w  (a1,d0.w),d4      ; d4 = cos(angle)
000214  move.w  $a(a7),d1
000218  muls.w  d4,d1
00021A  move.w  $8(a7),d2
00021E  muls.w  d3,d2
000220  sub.l   d2,d1             ; x*cos - y*sin
000222  add.l   d1,d1
000224  add.l   d1,d1             ; << 2
000226  swap    d1                ; >> 16   -> /16384
000228  move.w  $a(a7),d0
00022C  move.w  d1,$a(a7)
000230  move.w  $8(a7),d1
000234  muls.w  d4,d1
000236  muls.w  d3,d0
000238  add.l   d0,d1             ; x*sin + y*cos
00023A  add.l   d1,d1
00023C  add.l   d1,d1
00023E  swap    d1
000240  move.w  d1,$8(a7)
000244  movea.l (a7)+,a1
000246  rts

512 entries for a full turn, amplitude 16,384 = 2¹⁴, and cosine is sine shifted by 128 entries — one table, no second table, and the quarter-turn offset done with an addi.w #$100 on the doubled index. The first sixteen words are

0, 201, 402, 603, 804, 1005, 1205, 1406, 1606, 1806, 2006, 2205, 2404, ...

498 of the 512 entries are exactly round(16384·sin(2π·n/512)) and the other 14 are one least-significant bit away — a table generated once, in 1992, by something that rounded a little differently from Python. The scaling after the multiply is <<2 then swap, which is a right shift of 14 — the table's own amplitude — done in three instructions with no divs.

LVO −102 is the same table with no work done: mask the angle, read sin and cos, return both plus the table pointer. That is the entry point for anything that wants to build its own matrix.

LVO −36 is the perspective divide, and it is the one with a story:

00015A  move.l  a0,-(a7)
00015C  movea.l $8(a7),a0
000164  move.w  $c(a0),d3         ; the screen centre, packed as two words
000168  swap    d3
00016A  move.w  $a(a0),d3
00016E  move.w  $8(a0),d1         ; z
000172  lsl.w   #$2,d1
000174  move.w  $4(a0),d2
000178  add.w   $6(a0),d2         ; the divisor
.retry: asr.w   #$1,d1            ; <-- the overflow path lands here
00017E  move.w  (a0),d0           ; x
000180  muls.w  d1,d0
000182  divs.w  d2,d0
000184  bvs.b   .retry            ; division overflowed: halve and try again
000186  asr.w   #$1,d0
000188  add.w   d3,d0
00018A  move.w  d0,(a0)           ; x'
00018C  move.w  $2(a0),d0         ; y
000190  muls.w  d1,d0
000192  divs.w  d2,d0
000194  bvs.b   .retry
000196  swap    d3
000198  asr.w   #$1,d0
00019A  add.w   d3,d0
00019C  move.w  d0,$2(a0)         ; y'

divs.w on a 68000 overflows whenever the quotient does not fit in sixteen bits, which for a perspective divide means whenever a vertex comes close to the camera. The routine's answer is to catch V, halve the numerator's scale and retry the same divide, giving up one bit of precision each time rather than clamping the vertex or dropping the polygon. bvs back to a label above the multiply means the retry re-reads x from memory, so both coordinates end up on whatever scale the harder of the two needed. That is a 1992 fixed-point renderer's answer to near-plane clipping, and it costs four instructions.

LVO −78 is a 16-bit integer square root by binary search — mulu.w d3,d3 against the target, bisecting between 0 and $FFFE, at most sixteen iterations. The game calls it once. A square root in a first-person engine with a shop and a taxi is almost certainly a distance, not a normal.

vector.library — the renderer, and it is the blitter

Four exports, and the game calls all four; LVO −42 twelve times. It is the only file on the disc that programs the Blitter, and it programs nothing else: 45 of its 47 absolute custom-chip references are Blitter registers (BLTCON0×5, BLTCON1×2, BLTAFWM, BLTALWM, the four channel pointers, the four modulos, BLTADAT×3, BLTBDAT, BLTSIZE×5), and it touches no bitplane pointer, no colour register and no copper list.

  • −30 computes an address into a bitmap: y × 40 for the row (so 320 pixels, 40 bytes a plane-row), + 0x36 for a header, + x × 4, + plane × 6, with three flag bits in D3 selecting which of those terms to include. It then calls its own vector at −24 with a parameter of 2 — the library's fourth standard slot, which it has repurposed as an internal entry point.
  • −36 frees two allocations hanging off a structure at $2A(a0) and $2E(a0), going through exec when the library is not in its own context.
  • −42 is the one the game leans on. It takes a byte at (a0) as an opcode, scales it by four and jumps through two dispatch tables at hunk 0x27EA and 0x280A — a prologue table and an epilogue table — with the work between them. It allocates its workspace on the stack (suba.l d0,a7), which is what a re-entrant renderer with a variable polygon count does.
  • −48 is the largest routine in the library, about 9,000 bytes, and it walks a display list: for each entry it reads a flag word at $14(a4), masks three bits out of it, follows a chain of $C(a4) pointers, and branches on $FFFF sentinels and on byte indices at $E(a4) and $F(a4) into a table hanging off $4(a5). It is the scene traversal.

So the shape of the renderer is: geometry in tridee, arithmetic in math, and every pixel through the Blitter — no span-filling loop, no chunky buffer, and therefore nothing for Akiko to convert (section 06).

tridee.library — geometry, and no hardware at all

Seven exports at −30 to −66, five of them called; zero references to any custom-chip register. It opens math.library at init. LVO −54 is called five times and −60 twice. This is the transform and projection layer between the object files and the blitter.

The object files

40 files, 1,896 to 37,650 bytes, in two IFF shapes (tools/x3g.py):

.x3d    FORM VCDO { EXVL, PLST }                      one object
.x3g    FORM O3DG { OFFS, FORM VCDO × N, [VFTX] }     a group

EXVL is the vertex table and decodes cleanly:

UWORD  vertex count
repeat:
    WORD  x, WORD y, WORD z     model space, signed, ±few hundred
    BYTE  scratch[10]           zero on disc

Sixteen bytes a vertex, of which six are the model and ten are run-time scratch that ships as zeros — the transformed coordinates go back into the same record. Across the 40 files there are 367 objects, and 2 + count × 16 reproduces the chunk length exactly on 294 of them. The other 73 carry a short even-length tail after the last vertex, 4 to 74 bytes:

0BankVectors.x3g   object 0, 8 vertices, 6 spare bytes:
    02 00 01 06 04 00
0CityMonsters.x3g  object 4, 32 vertices, 12 spare bytes:
    00 05 06 15 07 0d 0a 09 10 08 0f 00

Those are byte-sized vertex indices: every value is inside the object's own vertex range on 42 of the 73, and 41 of the 73 end in 00. A short index list appended to a vertex table is a hull, an outline or a set of attachment points; which is not settled (12-open-questions.md).

PLST is the polygon list and is only partly read. Its first two words are 0 and one of 0x28, 0x2C, 0x2E — a header length — and the four words before the first polygon are the object's screen-space bounding box. The polygon records themselves are not decoded; see 12-open-questions.md.

VFTX appears once, appended to Objects.x3g after the last VCDO, and it is the whole content of /Demo.vfx. It is a table of byte pairs (9f 1f 9e 1f 9e 1e 9e 1d …) where the first of each pair has bit 7 set — a texture or shading table, not decoded.

The geometry, measured

Vertex counts and bounding boxes, from notes/sprite-banks.md:

Family objects vertices (level 0)
CityVectors 33 519
RoomVectors 26 632
CityMonsters 9 302
Droids 6 251
ShopVectors 4 104
people 4 145
BarVectors, BankVectors 3 each 70, 31
Monsters…Monsters4 2–3 each 70–116
Objects 3 12
PoliceVectors 2 49

The city is built out of 33 objects whose bounding boxes are all ±82 in x and z, and whose y runs from −80 to about −77. A 164-unit square footprint repeated 33 ways is a tile set: the city is a grid, and CityGen chooses which tile goes in which cell (section 09).

The 0/1/2/3 prefixes are not four levels of detail

Seven families exist four times over, prefixed 0 to 3, and the game's filename table holds only the 0 form (:0CityVectors.x3g), which it patches with the digit it wants. The obvious reading is four levels of detail — and it is wrong:

0 1 2 3
BankVectors vertices 31 28 31 28
BankVectors PLST bytes 920 788 920 788
BankVectors file size 4,594 4,414 3,336 3,156
CityVectors vertices 519 341 519 341
RoomVectors vertices 632 604 632 604

Sets 0 and 2 have identical geometry, and so do 1 and 3. The vertex counts and the polygon-list lengths match to the byte across all seven families; only the file sizes differ, by 15–30 %. So the four sets are two geometries × two of something else — the second axis is inside PLST and is almost certainly the shading or texture data, since that is where the surplus bytes are. Two detail levels and two visual qualities, not four detail levels. What the second axis selects is unresolved.

/BankVectors.x3g with no prefix is byte-identical to /0BankVectors.x3g (SHA-1 14d4bdc9b8c1…); /BarVectors.x3g and /ShopVectors.x3g are unprefixed files with no prefixed twin that is identical, and nothing on the disc names any of the three. They are the pre-split originals, left behind (section 11).