prosolis 91e2e9803d Take the project's own story out of the README, starting with a claim about elapsed time
USER CORRECTION: the README is a description, not a chronicle, and the intro
had it narrating an arc -- "for most of this project's life that was read as a
video codec problem" -- which is also a claim about elapsed time that nothing
measured supports. The tree is days old.

The intro now states the design: it is a delivery problem, the live design has
no decoder in it, and the comparison that settles it is a measurement (37,585
decoded costs more than 49,152 not decoded; 110.4% against 54.9% at the 9 clk/B
floor). The codec is "in the tree and parked (USER DECISION)" -- no session
number attached, because which sitting it happened in is STATUS's business.

Four more of the same, found by grepping rather than by eye:
  - B2 "is not a headroom question any more" -> it decides which player exists
    rather than how much headroom one has.
  - "Other things the machine has said" -> "What else a player has to carry".
  - ring.i/xfer.i/stream.s "left the video path when a DMAC-direct player
    deleted the ring" -> a DMAC-direct player has no use for them, because it
    has no ring. Same edit in the parked list and in Layout.
  - "Why it is parked" kept the standing reason and dropped the retelling.

And one where the story was carrying a fact that had to stay: the sasi profile
was "dropped on capacity rather than bandwidth" with the numbers left in
FINDINGS. They are in the sentence now -- a 40 MB volume against 146 MiB of
game -- so the clause survives without the reader needing the episode.

One "session" reference stays, at the STATUS.md bullet: that file IS ordered
newest session first, and saying so is navigation for another document rather
than this one telling a story.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 16:59:04 -07:00

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 it is a delivery problem rather than a compression one — bus cycles and bytes off a disc.

The live design has no decoder in it. The disc holds packed 8-bit frames and a DMA channel walks them into GVRAM with the CPU halted. Measured on the machine, decoding 37,585 bytes costs more than not decoding 49,152: 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%.

A working codec is in the tree and is parked (USER DECISION). 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 (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).

Blu-ray source next to the packed player's own screen

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? — decides which player exists rather than how much headroom one has, and K4 is not built until it is answered (FINDINGS 64.2, ROADMAP K4).

What else 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 — 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 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. Its remaining path is a conjunction and the packed one is not: reaching a fitting rate needs a 35% byte reduction, two of its three levers measure inert, and the reward on success is a design at ~100% of the frame against the packed player's 54.9%. 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.71.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 a DMAC-direct player has no use for because it has no 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
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:

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 <c.dlx>      # 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:

# 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).

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:

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 — a SASI volume is 40 MB and the game is 146 MiB even at that rate. 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: a DMAC-direct player has no 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.

S
Description
No description provided
Readme
25 MiB
Languages
Python 53.1%
Assembly 20.4%
Lua 15.3%
Shell 8.8%
C 2.3%
Other 0.1%