# Dragon's Lair: Sharp X68000 port
Porting Dragon's Lair to a stock X68000 (68000 @ 10MHz, 2MB, SCSI).
The game logic is a scene table with branching input windows. The difficulty is
pushing ~22 minutes of Don Bluth animation through a 10MHz 68000, and for most
of this project's life that was read as **a video codec problem**. It is not.
Measured on the machine, the codec costs more to *decode* than a literal frame
costs to *not decode*, so the live design has **no decoder in it at all**: the
disc holds packed 8-bit frames and a DMA channel walks them into GVRAM with the
CPU halted. It is a **delivery** problem — bus cycles and bytes off a disc.
The codec still exists, still works and is **parked** (USER DECISION, session
29). Both branches are described below; the packed one is where the work goes.
## What it looks like
**The packed player, running off a real volume, with sound.** `src/player/packed.s`
on an emulated stock X68000: the 68000 brings up its own display, builds its own
193-entry DMA chain, keeps its own frame clock off the CRTC's V-DISP, and fetches
every record itself with `READ(10)` from a real MB89352. **There is no decoder in
the machine.** Source on the left; on the right, MAME's own snapshots of what the
68000 put on screen. **120 of 120 frames are pixel-exact against the container**,
every one compared, and the clip refuses to build otherwise.
[`docs/img/packed-player.webm`](docs/img/packed-player.webm) (120 frames, 12 fps,
VP9, with the chip's own audio)
**The clip is a composite of two runs and that is the point.** The picture is the
gate run — paced at half rate so each snapshot lands inside the write window,
cycle stealing, no sound. The sound is the audio run — the same container at
12 fps with the MSM6258 on channel 3, captured by MAME off the speaker, cut at
the first sample the chip produced and gated sample-exact against lump 0 before
anything is written. **What it is not is a real-time capture of the shipping
configuration**, which at this container's own burst rate would show a blank
layer for 99.5% of every slot (FINDINGS 64.2, below).

The still is the frame whose PSNR is closest to the mean over the gated window,
so it is not the flattering one: **33.13 dB against the window's mean of 33.10**,
which is the packed container's predicted GRB555 figure to the digit.
**Name the layer.** Everything here is **emulated**: MAME 0.277 `x68000`,
`-bios ipl10`, stock 10 MHz / 2 MB, with every 68000 cycle figure cross-checked
on a second CPU core (px68k's C68K) and the display path cross-checked against
px68k's own `gvram.c`. **Nothing in this project has run on real hardware yet.**
## How the packed player works
**256-colour GVRAM throws away the high byte of every word a CPU writes**, so a
picture byte normally costs two disc bytes. **CRTC R20 bit 11 turns the masking
off**, and with the two 256-colour pages scrolled apart one word carries two
pixels (FINDINGS 46/47). That makes a frame 1.0 B/pixel — 49,152 bytes — and it
makes the frame a *literal*: no codebook, no recursion, no decode.
A record is that picture plus its own 256-entry palette: **49,664 bytes, which is
97 sectors exactly**. A packed record's length is geometry, so **the container
carries no index and no length word** — record *i* is at `off + i*rec` and a seek
is arithmetic. A DMA channel fills GVRAM in buffer mode straight off the disc and
**walks the 1,024-byte line stride itself** through array chaining, so a frame is
**one channel start and not 192** — and the chain crosses from the palette
registers at `$E82000` into GVRAM in the same start (FINDINGS 62). The array is
scene-constant, because the packed layout spends both 256-colour pages and there
is no page to flip.
**Audio rides a fixed cadence, not the record.** The MSM6258 wants 7,812.5 B/s
and 12 fps wants 651.0417 B a slot, a ratio with no arithmetic relationship, so
audio in the record would make records variable-length and bring the index back.
DLXP2 groups instead: **one 14-sector audio lump, then 11 records**, so
`record i = off_frm + i*rec + (i//11)*7168` — six instructions once a frame. The
lump is 7,168 bytes of space and eleven frames of audio is 7,161.4583…, so **the
payload alternates 7,161 and 7,162** and a player that fed the chip whole lumps
runs 0.09% fast: **1.25 s of lip-sync drift over the game's 22.8 minutes**,
predicted, and played at 1.26 (FINDINGS 67.2/68.3). The player carries one
accumulator instead, which is the frame clock's shape for the frame clock's
reason.
**The wire is fixed and there is no lever on it: 582.0 KB/s of picture +
7.64 KB/s of audio cadence = 589.6 KB/s.** A codec's bitrate is adjustable; a
literal frame's is geometry, and no scene costs less than another.
| | |
|---|---:|
| record | 49,664 B = 97 sectors |
| wire | **589.6 KB/s** |
| picture, as the display renders it | **33.10 dB** mean over the gate window |
| CPU per frame, video path | one channel start and one `READ(10)` — no paint |
| cost at the 9 clk/B dual-address floor | **54.9% of a 12 fps frame** (FINDINGS 64.2) |
| the codec, at the same floor | 110.4% (FINDINGS 61.4) |
## Where it stands
**What runs, end to end, on the emulated machine off a real volume:**
- **120 of 120 frames pixel-exact**, in both palette orders, every frame compared
rather than the last — a packed frame is a literal, so frame 119 says nothing
about frame 60 (FINDINGS 64.1).
- **All 78,125 B of a container's audio, read back off the speaker**, sample-exact
against the four ADPCM axes carried in the container's own header, one
delivered byte at a time — because a player's counters all stay right through
a bug that overwrites the buffer the channel is reading (FINDINGS 68.1).
- **A mid-stream seek with sound on it**: `pg_aseek` rebuilds the lump index, the
stream position, the remainder accumulator and the byte offset into the group
and issues a second read — **132,162 B of spliced stream accounted for byte by
byte** across a branch deliberately not on a group boundary (FINDINGS 71.1).
- **The scene graph**: 40 scenes, 516 sequences, 906 input windows, imported from
the arcade's own graph into this project's schema (FINDINGS 56).
**The binding resource is the 68000's local BUS, not its clock**, and every item
above is priced in one of four units:
| resource | state |
|---|---|
| **68000 local bus** | the binding one. The codec's decoder occupies 86.7% of it, and 52 of the 53 frames that miss the 12 fps budget miss on the bus (FINDINGS 38). |
| **68000 clocks** | measured, on two independent cores. |
| **delivery rate** | **no working figure, deliberately** (FINDINGS 50, USER DECISION). `--bus`, `--kbps` and `DLX_STREAM_KBPS` are required arguments with no defaults, so no table can be scored against a rate its own output does not state. |
| **seek time** | **no figure at all, and never had one.** |
| **W, clocks stolen per delivered byte** | 5 single-address held, 9 dual held, 12 single arbitrated, 16..19 for the IPL ROM's own disk channel. **Still the largest open number.** The CPU doing the transfer itself is **87.28 clocks a byte, measured** — 4.6x the worst DMA configuration found here and 17.5x the best (FINDINGS 58). |
### The open question that decides which player gets built
**The write window is the frame.** A packed write *requires* R20 bit 11; buffer
mode blanks the layer while it is set; and a DMAC-direct player holds the window
open for the whole data phase, because there is no second page to hide behind.
dark fraction of a slot = record bytes / (DATA-PHASE rate x slot)
**The rate in that expression is the BURST rate, not the sustained one**, and at
the container's own 582.0 KB/s the dark fraction is **1.0**: every frame
delivered, on time, pixel-exact, and none of them displayed. Free-running, the
run reported exactly that — **the window was open on 99.5% of host frames**.
It **reverses the ranking** in clocks, so there are two packed players and the
difference between them is *when* the window is open:
| | clocks (W=9) | on screen | RAM |
|---|---:|---:|---:|
| **A — DMAC-direct** (built, FINDINGS 64) | **54.9%** of a slot | 0% at the container's wire, 72.7% only at 2,131 KB/s | none |
| **B — DMA to RAM + CPU paint** (K4, not built) | 82.2% | **72.7% at any rate** | 99,328 B |
B's paint is **measured**: the packed `movem` blit is 227,553 clocks, 27.3% of a
slot, independent of the medium. They are equally visible only at **3.7x the
wire**. So B2 — *does buffer mode blank the display?* — is not a headroom
question any more; it decides which player exists, and **K4 is not being built
until it is answered** (FINDINGS 64.2, ROADMAP K4).
### Other things the machine has said, that a player has to carry
**A held channel costs the frame clock half its ticks, without the clock being
able to tell.** `clock.i` counts V-DISP interrupts; a held channel halts the
68000; the MFP's pending bit is one bit. Held at 12 fps, **487 of 1,038 edges are
lost** and the player reports **zero late frames**, because the tick it grades
itself against is advanced by the interrupt the channel stopped it from taking.
It believed it was at 12 fps; the screen was at **6.37** (FINDINGS 64.3).
**Held, the bus costs the audio 463 times the seam.** The MSM6258 has no
starvation state — it goes on decoding nibbles out of whatever byte its data
register still holds — so the interval between a channel counting out and the CPU
arming the next lump is a held nibble pair driving the predictor. Stealing, that
seam is **0.51 ms over ten seconds**, because the audio service runs from inside
`dma.i`'s transfer wait. Held, it is **236 ms, 2.31% of the audio**, with every
byte still correct (FINDINGS 68.2).
**The chip is not the datasheet and not ffmpeg, and it was asked.** Sixteen
candidate decoder models were fitted to what the emulated MSM6258 produced from a
designed nibble stream; **exactly one reproduces it sample-exact**, with a
negative control on every axis. The chip runs the datasheet's truncation, takes
the **low** nibble first, clamps its accumulator at **10 bits** and starts it at
**−2**. Getting the delta formula wrong costs −2.88 dB; **getting the nibble order
wrong costs −25.74 dB** (FINDINGS 66). The four axes ride in the container header
as fields rather than a version number.
**The audio level is measured off the whole disc and does not change.** All 201
streams that have audio, 21.5 minutes: the **disc peaks at 946 of 2048, 5.35 dB
over the chip's clamp**, in 402 events totalling 44.0 ms. Forty windows encoded at
six gains price the choice, and the disc's own level has the **best mean SNR
(22.03 dB)** — the gain that guarantees zero clamping costs 0.85 dB across the
game to buy back 1.90 dB on the 2.11 seconds that clamp, because the OKI step
table's floor is a constant and does not scale (FINDINGS 69).
**The predictor does not seek.** The MSM6258's accumulator is a pure integrator
with no leakage term, so a branch that hands the chip bytes chosen for a state it
is not in produces **a DC offset that does not decay**: playing through, −355 of
511 with AC 0.00, still −108 four seconds later; stopping and re-PLAYing, a single
permanent constant of −65. **A re-PLAY is 5.5x better and neither is zero.** The
only fix that reaches zero is the encoder's — reset the predictor where a branch
can land — and it costs **0.33 dB** (21.99 → 21.66) because the step table
re-converges in a few samples (FINDINGS 71).
**Branch points do not wait for the buffer.** 5.4% of the game's 612 branch
transitions open an input window on the first frame of a clip the disc *seeked
to*, so two seeks can fall back to back with no play between them. On the packed
branch there is **no refill climb at all** — a record goes straight into GVRAM and
the video lookahead is zero records — so the only consumer with any lookahead is
the audio one: **1.833 s of sound held against 0.000 s of picture** (FINDINGS
56/70.2). And a DLXP2 group puts its lump *in front* of its records, so a seek
lands with its audio behind it: **mean 416.5 ms of silence entering a branch**
over the arcade's 409 within-container targets, which is what `pg_aseek`'s second
read removes for 11.7 ms (FINDINGS 70.3).
### The parked branch: the codec
The codec is a Cinepak-style hybrid — each 4x4 block coded as SKIP, one 4x4
codeword, four 2x2 codewords, or RAW literal pixels, chosen per block by
rate-distortion, with a v7 literal-span mode over the top. It works, it is
measured, and it is **pixel-exact on the 68000 under two independent CPU cores**;
`stream.s` decodes a 120-frame window out of a 256 KB ring on a stock 2 MB
machine with the container in a file rather than in RAM.
[`docs/img/player.webm`](docs/img/player.webm) — the codec player, 119 frames,
12 fps. 116 are pixel-exact against the reference decoder; **three are torn**,
frame *n* on top of frame *n-1*, because MAME captured the screen while the block
loop was partway down it. `decode.s` writes straight to the displayed page, so a
real player tears the same way, and the media builder **asserts the tear** rather
than trimming it. [`docs/img/modes.webm`](docs/img/modes.webm) is the same window
with the block-mode map beside it — black SKIP, blue V1, amber V4, red RAW —
which is what every cost table in FINDINGS is really about.
**Why it is parked.** At the 9 clk/B dual-address floor the codec is **110.4% of a
12 fps frame and the decoder-free packed player is 54.9%**: decoding 37,585 bytes
costs more than not decoding 49,152. Reaching a fitting rate needs a 35% byte
reduction after two of its three levers were measured and found inert, and the
reward on success is a design at ~100% of the frame. **It is kept on disk and not
built on** — if B2 comes back "buffer mode blanks", it is the only thing left
(FINDINGS 61.8, 48.3).
## What is open
**Hardware — this list is the user's, and nothing here can be settled by an
emulator.**
- **B1. Measure the medium.** Three thresholds, not one: **sustained ≥ 589.6 KB/s**
or frames arrive late; **the data-phase BURST rate**, which decides how much of
the slot the picture is on screen; and **seek time**, which has no figure at
all. Plus what one extra SCSI command costs, which decides the audio cadence.
The 0.7–1.7 MB/s usually quoted for BlueSCSI on an X68000 is **folklore with no
published benchmark behind it**.
- **B2. Does buffer mode blank the display?** `probe_bit11_blank.lua` is written
and settles it in minutes on a real board. It decides A vs B above.
- **B3. Does a real CZ-6BS1 drive `#EXREQ`?** MAME's card has no request line to
the DMAC at all, so every configuration that can be run against it is
auto-requested and **charged by time rather than by byte**. A real request line
is what single-address 5 clk/B needs.
- **B4. Does a real palette register take a byte write?** A negative answer costs
0.28% of a frame and nothing else.
- **The MSM6258V, on silicon**: the four axes; whether it resets accumulator, step
index and nibble select on PLAY only when it was not already playing; and
whether it goes on asserting `#DRQ` while STOPped.
**Software, in order.**
1. **The predictor-reset container (DLXP3).** The cheapest thing that takes a
measured cost to zero rather than down. The player half already exists
(`PG_ARST`).
2. **The audio buffering depth.** `PG_ANBUF` is 3; two slots is one constant and
one run. The audio buffer is the packed branch's *only* buffer.
3. **The silent-clip and short-audio cases**: a scene whose audio is shorter than
its frames, and a scene with no audio track at all.
4. **The cadence pick.** With a working seek path the silence F=11 costs is zero,
so the trade is padding against RAM — plus one SCSI command per branch, which
is B1's.
**Parked, so it is not re-proposed:** the codec's remaining encoder work (E7, E4,
C1); `ring.i`, `xfer.i` and most of `stream.s`, which left the video path when a
DMAC-direct player deleted the ring, and P4a's wiring with them. **K4 is blocked
on B2**, not parked.
## Reproducing this
**No media ships in this repo and none of it is redistributable.** Bring your own
Dragon's Lair Blu-ray. Everything else needed to rebuild every number and every
picture above is either here or is packaged.
| | |
|---|---|
| the disc | loop-mounted read-only: `udisksctl loop-setup -r -f DRAGONS_LAIR.iso`. Built against a decrypted UDF 2.x image; 7-Zip cannot read UDF 2.x, so use the loop mount |
| `python3` | plus **numpy** and **Pillow**, and nothing else. The k-means is hand-rolled rather than pulling in sklearn |
| `ffmpeg` / `ffprobe` | frame extraction, and the clips above |
| **MAME** | tested on 0.277, with the `x68000` ROM set. The rigs drive it headless via `-autoboot_script` |
| vasm (m68k, Motorola syntax) | **vendored**: `tools/vasm/vasmm68k_mot` is a Linux x86-64 binary, with the source tarball beside it to rebuild elsewhere |
```sh
export DLX_BDROM=/path/to/your/mounted/bluray # if not /media/$USER/BDROM
./tools/bench/check.sh # the green light, prints ALL GREEN
```
`check.sh` re-runs everything above that a host can re-run: both display
regression tests, the rate-control drift gate, the display-path coherency
counterexample, a 120-frame 68000 decode on two CPU cores, the ring and paced-ring
passes, the DMAC configuration gate, the load-time transforms on both cores, the
scene-graph import when a DirkSimple checkout is present, the packed container
(with audio, and a silent control beside it) rendered through px68k's own GVRAM
model, **the packed player for 120 frames off a real volume with every frame
compared**, the audio encoder against ffmpeg's decoder, the sixteen-way decoder
identification against the emulated chip, the DLXP2 container and each of its four
axes with a negative control on each, the audio level off every stream of the
game's own footage, and the refill climb against the arcade's own 612 branch
points. `DLX_BDROM` is honoured by every tool that reads the disc.
Two stages **skip rather than fail** when their input is absent, because both live
outside this repo: `PX68K=/path/to/px68k` for the second-CPU-core gate — the
cheapest strong test in the tree, and what licenses the bus and cycle figures —
and `IPLROM=/path/to/iplrom.dat` for the DMAC configuration gate.
The player runs and the media are rebuilt from them:
```sh
bash tools/bench/packed_run.sh # the packed player, 7 runs
python3 tools/media/make_packed_media.py # docs/img/packed-player.{png,webm}
python3 tools/media/make_readme_media.py # the parked codec's stills and clips
```
Both media builders **gate before they write**: a still or a clip of the player is
a claim that the player drew it, so every frame is checked pixel-exact against the
container first and the audio cut is checked sample-exact against lump 0. A README
that illustrated a pixel-exact player with an approximate picture would be a small
lie about the one property this project keeps testing.
**Scene selection is a hard-coded stream number, not a search.** The gates use
streams `00020` and `00223` of the disc's 224 `.m2ts` files. A different pressing
may number them differently, and if so the green light will extract the wrong
footage rather than fail, so check that `tmp/fr_singe/` looks like the Singe
encounter before trusting any figure. **Not every large stream is game footage**:
`00216` is the feature with a burned-in commentary picture-in-picture and `00215`
is the commentary itself, the two largest files on the disc. The clean
9.4-minute animation is **`00223`** (FINDINGS 25.1).
**Nothing outside-derived is committed here.** The scene graph is regenerated from
a reader's own clones into gitignored `tmp/`, and `tools/import/scenegraph.py` is
the single file in the repo coupled to those projects — everything downstream reads
`DLXSCENE1`, this project's own schema, with the sources' attribution carried in
it. DirkSimple is zlib (Ryan C. Gordon); the SNES chapter set is MIT (Chad
Doebelin) and, by its own README, *derived* from DirkSimple rather than an
independent transcription.
## Encoders
**The live one is the packed container**, and it has no rate control in it because
there is no rate lever:
```sh
# the gate window: stream 00223, 10.0 s from 539.4 s, the same seconds both times
python3 tools/encoder/extract.py 00223 tmp/fr_singe 12 crop 539.4 10.0
python3 tools/encoder/extract_audio.py 00223 tmp/au_singe.raw 15625 539.4 10.0
python3 tools/encoder/pack.py tmp/fr_singe out.dlxp --audio tmp/au_singe.raw
```
`dlxp.py` is the format and the one place the interleave, the 97-sector record,
the audio cadence and the lump payload are stated. It encodes in ~3 seconds
because there is no k-means in it. `--scene-palette --no-palette` is the fallback
container: **30.79 dB, zero palette churn, 576.0 KB/s**, still ahead of the codec
as the display renders both, and it is what ships if B2 says the layer blanks —
because a per-frame palette changes **231 of 256 entries every frame**, and a
picture under the neighbouring frame's palette is **12.8 dB worse**
([`docs/img/palette-mismatch.png`](docs/img/palette-mismatch.png)).
`adpcm.py` is the MSM6258 codec and it carries **two decoders on purpose**: the
module defaults are ffmpeg's, so `tools/bench/verify_adpcm.py` stays a check
against an independent implementation, and `adpcm.CHIP` is the set measured out of
the machine's own chip. Anything that encodes *for* the machine passes `CHIP`
explicitly.
**The parked codec's encoder is kept and still runs:**
```sh
python3 tools/encoder/encode.py tmp/fr_singe out.dlx --profile scsi --preview p.png
```
`--kbps` is the quality rate point and `--span-kbps` the ceiling the span pass may
draw on; `mu` is bisected per frame against the 68000's own decode budget so a
frame also *decodes* in time. One profile, `scsi`; the 110 KB/s `sasi` profile was
dropped on capacity rather than bandwidth. The RAW escape means `lam=0` is
pixel-exact against the palettised frame. See FINDINGS 31/41/44 before changing
any of it.
## Documentation
- **`docs/STATUS.md`** is the current state, working setup, blockers and next
steps, newest session first. **Start here.** It also lists what has been
explicitly abandoned, so old ideas do not get re-proposed.
- **`docs/ROADMAP.md`** is the remaining work to a completion target. Read it with
STATUS rather than instead of it: STATUS holds the measurements, ROADMAP holds
the shape and goes stale first.
- **`docs/FINDINGS.md`** is measured hardware facts, content statistics, the codec
decision, and a section on measurement traps that produced three separate false
results. Read §4 before trusting any pipeline number. It is append-only and
later sections overturn earlier ones; superseded sections carry a blockquote
pointing at the correction.
- **`docs/BENCHMARK.md`** is how to measure the storage subsystem, and why a
bandwidth figure out of MAME would be meaningless.
- **`docs/HARDWARE.md`** is the X68000 GVRAM/CRTC reference.
## Layout
```
src/player/ packed.s THE LIVE PLAYER: display bring-up, the 193-entry DMA
chain, its own V-DISP clock, READ(10) off a real
volume, and the MSM6258 on channel 3 -- a lump ring,
the remainder accumulator that stops the padding
becoming drift, a service routine that runs from
INSIDE dma.i's transfer wait (0.51 ms of seam
against 236), and pg_aseek.
dma.i the HD63450 channel: array chaining, held and
stealing, and DM_HOOK.
scsi.i the MB89352: selection, READ(10), the data phase.
adpcm.i the audio transport, every register byte of it
decoded out of the IPL ROM rather than recalled.
clock.i the frame clock, off the CRTC's V-DISP.
geom.i the constants, in one place.
decode.s the parked codec's 68000 decoder (1,296 bytes,
stream.s asserted), the same decoder behind a bounded ring,
ring.i the ring producer, the transport under it, and the
xfer.i load-time codebook/palette transforms. OUT OF THE
load.i VIDEO PATH since the packed player deleted the ring.
frame.i the block loop and span chain, included by both, so
there is exactly ONE copy of the bytes every cycle
constant is fitted to. The span pass is blit.s v7
verbatim -- do not tidy it.
tools/encoder/ dlxp.py THE LIVE CONTAINER (DLXP2) and pack.py writes it.
pack.py Nothing is shared with the codec's writer on purpose.
adpcm.py the MSM6258 codec, carrying ffmpeg's decoder and the
chip's measured one side by side.
dlx.py the codec's container and its REFERENCE DECODER,
encode.py ground truth for the 68000; the encoder, rate
spans.py control, and the v7 span geometry. Parked, kept.
extract.py frames and audio off the disc.
tools/bench/ check.sh the green light.
packed_run.sh the packed player: gate, rate, audio, held, seek.
verify_packed.py / verify_packed_audio.py
the two gates that matter. The audio one reads the
SPEAKER, one delivered byte at a time, because every
counter in the player stayed right through a bug
that overwrote the buffer the channel was reading.
crtc_mode.lua the SINGLE SOURCE OF TRUTH for CRTC R00-R08 and
R20. Do not write CRTC values anywhere else.
probe_bit11_blank.lua B2, ready to run on a real board.
c68k/ a SECOND emulator for every 68000 cycle figure:
px68k's CPU core, no SDL, no ROMs. It also counts
BUS cycles, which MAME cannot report.
gvpack/ the same argument for the DISPLAY: px68k's own
x68k/gvram.c, so the address decode, the R20 bit-11
write path and the scroll wrap are its code and not
a model of it.
blit.s the full-frame GVRAM blit, timed on the 68000
itself. V8 is the packed one: 227,553 clocks.
tools/analysis/ measurement scripts, numbered in the order they were written.
Run from the repo root. 01 and 02 are marked BROKEN
deliberately and kept as regression references; 10 is a
COUNTEREXAMPLE and exits non-zero by design, demonstrating that
the two-display-path plan corrupts 70 of 120 frames. 15 is the
bus occupancy model, 19 the ring's ADDRESSES (not its
occupancy), 21 the IPL ROM's DMAC configuration, 25 the scene
graph, 30/34 the packed containers, 31 the display duty, 35 the
audio level off the whole disc, 36 the climb against real
branch points, 37 what a branch costs the chip. buscost.py is
the shared bus-cycle table; the per-block constants live in
vq_hybrid.py and are imported, never copied.
tools/import/ scenegraph.py -- the ONLY code in this tree coupled to somebody
else's source. Output is gitignored derived data.
tools/media/ make_packed_media.py builds the live player's still and clip;
make_readme_media.py builds the parked codec's. Both gate
before they write.
tools/vasm/ vasm m68k assembler, binary plus source tarball.
docs/img/ the stills and clips above, built from real emulated runs.
assets/, tmp/ extracted frames, audio and run output (gitignored).
```
Source media (`DRAGONS_LAIR.iso`) and ROMs are gitignored. Supply your own.