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
477 lines
28 KiB
Markdown
477 lines
28 KiB
Markdown
# 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.
|
||
|
||
<video src="docs/img/packed-player.webm" controls muted loop width="100%"></video>
|
||
|
||
[`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?* — 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.
|
||
|
||
<video src="docs/img/player.webm" controls muted loop width="100%"></video>
|
||
|
||
[`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.** 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.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 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 |
|
||
|
||
```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 <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:
|
||
|
||
```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 — 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.
|