python3 tools/hunkinfo.py <file> → notes/hunkinfo.txt.
24 files begin 0x000003F3. Seven are Commodore commands in /c, four are the
shipped libraries, three are font files (which are hunk files by design), and
ten are game programs.
Step 36: a file beginning 0x000003F3 need not be a program, and the tell is
the ratio of relocations to code bytes.
| program | bytes | CODE | DATA | BSS | relocs | per KB of code |
|---|---|---|---|---|---|---|
SuperFrog/SuperFrogCD |
363,744 | 211,476 | 116,040 | 1,230,088 | 8,996 | 43.56 |
SuperFrog/SFIntro |
751,852 | 53,656 | 690,968 | 403,120 | 1,730 | 33.02 |
s/T17Menu |
125,368 | 1,452 | 123,620 | 0 | 55 | 38.79 |
Pool/POOL |
158,708 | 132,328 | 20,028 | 0 | 1,569 | 12.14 |
SuperFrog/T17Man |
24,172 | 22,732 | 980 | 0 | 90 | 4.05 |
SuperStardust/MAINDEMO1 |
528,316 | 506,968 | 0 | 1,001,192 | 5,318 | 10.74 |
SuperStardust/MAINDEMO2 |
528,524 | 507,188 | 0 | 1,001,192 | 5,315 | 10.73 |
SuperStardust/TUNNELDEMO |
1,479,404 | 1,475,712 | 0 | 278,016 | 904 | 0.63 |
Two entries stand out at the two ends.
TUNNELDEMO at 0.63 relocations per KB is seventy times below the disc's
own densest program and is the cd32rez signature from Gunship, on a different
disc, a different studio and a different country. All 904 relocations fall in
hunk offsets 0x8–0x90F8, that is in the first 2.51 % of a 1,475,712-byte
"code" hunk. The remaining 1.44 MB is data living in a CODE hunk. Independent
confirmation: 27 IFF 8SVX samples are embedded in it, the largest at resident
offset 1,378,212 (08-audio.md) — deep inside the region with no
relocations.
So step 36 now has two instances across two discs, and the diagnostic is the ratio, not the magic.
T17Menu at 1,452 bytes of code against 123,620 of data is the same
observation from the other side: a program that is 98.8 % data. Its 55
relocations all fall in hunk offsets 0x2C–0x566, inside the code hunk. It is
a real program — it has enough relocations for its size — but almost all of the
file is the menu's artwork.
SuperFrogCD at 43.56 per KB is the normal-program baseline for this disc, and
its relocations span 0x2–0x33A06, i.e. the whole code hunk, which is what a
real program looks like.
Two discs in the set (Legends, Universe) re-implement LoadSeg. Checked here:
every non-packed program file on this disc begins 0x000003F3, and no file
whose first longword is not 0x000003F3 and which is not a validated packed
stream has a first longword that is a valid in-file offset. Nothing on this disc
loads its own executables.
python3 tools/dispatch.py over all 311 resident pieces
(notes/dispatch.txt): 309 pieces return zero, two
return seven — SuperFrogCD and SFData/PARTF, which turn out to hold two
builds of the same engine.
The site, disassembled from the bytes at resident offset 0xEB70 of
SuperFrog_SuperFrogCD+gap0.bin:
00eb68 43 fa 0c 70 lea.l $f7da(pc), a1 ; table of longword deltas
00eb6c 47 fa 0c 78 lea.l $f7e6(pc), a3 ; the handler table
00eb70 42 a8 00 0e clr.l $e(a0)
00eb74 d5 d9 adda.l (a1)+, a2 ; step a2 to the next record
00eb76 30 12 move.w (a2), d0 ; read the record's first word
00eb78 02 40 00 3f andi.w #$3f, d0 ; low six bits select a handler
00eb7c d0 40 add.w d0, d0
00eb7e d0 40 add.w d0, d0 ; x4
00eb80 42 87 clr.l d7
00eb82 4e b3 00 00 jsr (a3, d0.w)
Read from the raw bytes, not from a disassembler's rendering of the constants (the checklist's warning about Capstone on this code applies; every constant above was re-read from the encoding).
Is this the Universe-style bytecode VM? No, and the difference is worth stating precisely because the encoding is superficially the same shape.
| Universe | Superfrog | |
|---|---|---|
| index derivation | neg.b + subq + andi #$ff — the complement of the opcode |
andi.w #$3f — the opcode's low six bits |
| what the pointer walks | a byte stream, advanced by each instruction's own length | records, advanced by a fixed table of longword deltas read with (a1)+ |
| program counter | explicit, saved and restored | none — a1 is a compile-time constant list |
| control flow | a loop over the stream | straight-line: adda, dispatch, test, adda, dispatch, … |
a1 is the giveaway. In a VM the step size comes from the data being
interpreted; here it comes from a table baked into the executable, so the
sequence of records visited is fixed at build time. This is a 64-way type
dispatch over object records, not an interpreter.
That is the alternative Gunship pointed at — game structure living in a data format rather than in bytecode — appearing on a platformer exactly where the checklist said to look for it.
P9 is therefore half right: no VM of the Universe form, and the count is reported rather than the absence merely asserted.
Five tables, 0xED0E, 0xEE0E, 0xEF0E, 0xF7E6, 0xF8E6, each 64 entries
of four bytes, every entry a bra.w — 320 slots. python3 tools/jumptable.py:
| table | distinct targets | default's share |
|---|---|---|
0xED0E |
17 | 48/64 = 75 % |
0xEE0E |
19 | 46/64 = 72 % |
0xEF0E |
18 | 47/64 = 73 % |
0xF7E6 |
2 | 62/64 = 97 % |
0xF8E6 |
2 | 62/64 = 97 % |
Across all five: 24 distinct handlers over 320 slots, and one address —
0x00F9E6 — takes 265 of the 320, or 82.8 %. So the opcode space is 64 wide
and the implemented vocabulary is 23 handlers; four fifths of every table is a
shared do-nothing default. The two 97 % tables are near-empty variants selected
by different object state.
That is the "empty slot" measurement the checklist asks for on string tables, applied to a jump table: the slots are there, most of them are unused, and the count of unused ones is the result.
SFData/PARTF unpacks to a 209,504-byte piece that holds the same seven
dispatch sites at a constant offset of 0x728 below SuperFrogCD's, and the
code at the site is byte-identical; over 4 KB from the dispatch point the
two agree in 3,802 of 4,096 bytes (92.8 %). So the particle/intro payload
PARTF embeds a variant build of the game engine rather than referencing it.
That is 209,504 resident bytes of engine stored twice, and it is invisible to a file-level duplicate check because one copy is inside a compressed container.
MAINDEMO1 and MAINDEMO2 are the case open item 23 asks about, and they show
why the naive comparison is useless.
Byte level. First difference at file offset 23, inside the hunk size
table: 01 EF 16 against 01 EF 4D, i.e. 126,742 against 126,797 longwords.
End-align the two code hunks and zero bytes match. A byte diff reports two
unrelated files.
Content level. The BSS hunk is identical (1,001,192 bytes both). The code
hunks are 506,968 and 507,188 — a 220-byte difference — and diverge at code
offset 0x24, which is lea $00016FFC,a0 against lea $000170D2,a0: an
address shifted by 214 bytes. Everything after that is shifted too, so nothing
matches.
Relocation-masked. Zero every longword covered by a RELOC32 entry (5,318
in MAINDEMO1, 5,315 in MAINDEMO2) and compare again:
the two code hunks agree for the first 6,083 bytes
then: A 33 fc 00 01 .... move.w #$0001,<relocated address>
B 33 fc 00 02 .... move.w #$0002,<relocated address>
The first real difference between the two builds is an immediate 1 against an immediate 2, almost certainly the demo number, at a point where the naive diff had already given up 6,059 bytes earlier.
So the procedure for the next disc: mask the relocations before diffing two builds. It turns "unrelated" into "one word". It is not perfect — non-relocated PC-relative displacements and absolute literals still shift, which is why the masked images agree for 6 KB rather than for the whole file — but it is the difference between a useless answer and a specific one.
The cleanest two-build pair in the set. Both 24,172 bytes, same length, so none of the size-table arithmetic above applies.
They differ in 10 bytes across 2 runs:
0x00039E 9 bytes "9.8.94)\0)" vs "10.8.94)\0" the $VER: date
0x0015E1 1 byte 0x00 vs 0x04
Read the second from the encoding. At file offset 0x15DE (CODE hunk starts at
file offset 36, so hunk offset 0x15BA):
2f 3c 80 00 00 32 move.l #$80000032,-(sp) ; SA_DisplayID
2f 3c 00 02 90 00 move.l #$00029000,-(sp) ; T17Man
2f 3c 00 02 90 04 move.l #$00029004,-(sp) ; T17Man.lace
$80000032 is SA_DisplayID (TAG_USER + 0x32), pushed with its value
alongside $80000029 (SA_Colors) and the rest of an OpenScreenTagList tag
array being built on the stack.
$00029000 is PAL_MONITOR_ID | HIRES = 0x00021000 | 0x8000. $00029004
adds LACE = 0x0004. So:
T17Manopens PAL:HighResT17Man.laceopens PAL:HighRes-Interlaced
One byte, and it is a DisplayID, not a BPLCON0 bit. Neither build is NTSC
— open item 23 asks for a PAL/NTSC pair and this is a PAL/PAL-interlaced pair,
which is a different axis. The disc ships both and the manual system picks
between them; nothing here adapts to the machine's video standard.
The version strings date the two a day apart, 9 and 10 August 1994, so the interlaced build is the later one.