Take the climb to a real branch point, and find the cadence has a third column
FINDINGS 70, and it closes ROADMAP P6. 65.6 recorded that "the slack table is here, but 51.3's refill climb with a second consumer through a real branch point is not", and 67.6 repeated it verbatim two sessions later. tools/analysis/36_branch_audio.py is that run: 51.3's surplus model, 56.3's 612 real branch points, 32's cadence arithmetic, both containers' headers and the buffer constants of the player 68 measured, in one place. No machine -- all five were already numbers. AUDIO IS 1.7% OF THE CODEC WIRE AND UP TO 3.30x OF THE CLIMB. Slack accrues out of pipe - wire, which is a small difference of two large numbers, so a second consumer is not priced by its share of the wire -- which is how every budget in this tree since FINDINGS 8 has quoted it. At 451.4 KB/s (the container's own zero-prefill pipe, 49.5) in a 256 KB ring the climb goes 6.70 s -> 22.11 s and the branch points arriving under it go 482/612 (79%) -> 603/612 (99%). At 488 it is 1.19x, at 600 it is 1.05x. Correction to 56.4: it charged audio at ratectl.AUDIO_KBPS = 7.8, which is 7,812.5 B/s in decimal kB; in binary KB the figure is 7.6294, so the placeholder was 2.24% high and every column moved in the flattering direction by less than one part in six hundred of the wire. AND ON THE PACKED BRANCH THERE IS NO CLIMB AT ALL. A record goes straight into GVRAM, so the video lookahead is zero records and 56.4's alarming column is vacuous when every frame arrives with less lookahead than the one before it. Acceptance there is a per-frame deadline, not an average, which is why B1 has to name a burst rate. The only consumer on that branch with any lookahead is the audio one: 1.833 s of sound held against 0.000 s of picture. And at 589.6 KB/s -- the acceptance figure this project quotes -- the sounded container's surplus is exactly zero: it plays and never banks a byte, while the silent one climbs its record in 6.38 s. THE COST NOBODY HAD COUNTED. A DLXP2 group is `lump k, then F records`, so lump k is at a LOWER address than every record of its group but the first, and a seek to record i finds its audio behind it. Measured on the 409 within-container seek targets of the arcade's own graph at the shipped F=11: mean 416.5 ms of silence entering a branch, median 416.7, p90 750.0, worst 833.3, and only 36 of 409 land on a group boundary. The other 203 -- the scene changes -- are free by construction, because lump 0 sits between the header and record 0. The container's own start is the one branch point the cadence costs nothing at, and it was the only one anybody had looked at. SO THE CADENCE PICK IS REOPENED, on a third column. 32_audio_wire chose F=11 on padding and RAM; F=1, the cadence it called the worst one, has no group to enter off-boundary at all, and gives back 12,288 B of RAM for +4.36 KB/s of wire (0.74% of the acceptance figure). The alternative is a second read: lump k is up to 503,808 B behind, so it is a separate command of 7,168 B -- 11.7 ms against 416, 36x cheaper in time -- and src/player/packed.s starts PG_AK/PG_AKF at lump 0 and has no audio seek path at all. Nothing here decides it; the deciding number is what one more SCSI command costs, and that is B1's. The content is not uniform mod F either: the measured mean runs 0.67x..1.23x of a uniform assumption across the sweep, and the 1.00x at F=11 is a coincidence. New check.sh stage, ~10 s, and it gates the STRUCTURE and deliberately not the milliseconds: audio never shortens a climb, the shipped cadence strands most within-container branches, F=1 strands none, and the lump read is an order of magnitude cheaper than the silence it removes. The one thing 70.3 and 70.4 rest on is a DESIGN ASSUMPTION and is printed in the tool's own output: one container per SCENE (53, 55.1, 56.3). One container per sequence makes every seek land on frame 0 and empties both sections. No encoder, player or container byte moved. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -481,6 +481,35 @@ What it removes is margin — at that rate in a 256 KB ring, **76% of this game'
|
|||||||
branch points arrive before the ring has refilled**, and a 512 KB ring makes it
|
branch points arrive before the ring has refilled**, and a 512 KB ring makes it
|
||||||
90%, because doubling the ceiling does not touch `pipe - wire` (FINDINGS 56).
|
90%, because doubling the ceiling does not touch `pipe - wire` (FINDINGS 56).
|
||||||
|
|
||||||
|
**And with the second stream on the wire, that climb is a different number.**
|
||||||
|
Slack accrues out of `pipe − wire`, which is a small difference of two large
|
||||||
|
numbers, so a second consumer is **not** priced by its share of the wire.
|
||||||
|
7.63 KB/s of audio is **1.7% of the codec container's wire and up to 3.30x of
|
||||||
|
its climb**: at 451.4 KB/s — the rate at which the *silent* container is exactly
|
||||||
|
deliverable — the climb goes **6.70 s → 22.11 s** and the branch points arriving
|
||||||
|
under it go **79% → 99%** of the game. On the packed branch there is **no climb
|
||||||
|
at all**, because a record goes straight into GVRAM and the video lookahead is
|
||||||
|
zero records; the only consumer there with any lookahead is the audio one,
|
||||||
|
**1.833 s of sound held against 0.000 s of picture**. And at **589.6 KB/s**, the
|
||||||
|
acceptance figure this project quotes, the sounded packed container's surplus is
|
||||||
|
exactly **zero** — it plays and never banks a byte, which is why B1 has to name
|
||||||
|
a burst rate and not only a sustained one.
|
||||||
|
|
||||||
|
**A DLXP2 group is `lump k, then F records`, so a seek finds its audio behind
|
||||||
|
it.** Read forward from record *i* and the next lump carries frame *(k+1)F*, so
|
||||||
|
the frames in between have picture and no sound. On the **409 within-container
|
||||||
|
seek targets of the arcade's own graph** at the shipped cadence F=11 that is a
|
||||||
|
**mean 416.5 ms of silence entering a branch, worst 833.3 ms, and 36 of 409
|
||||||
|
free**; the other 203 branch points — the scene changes — are free by
|
||||||
|
construction, because lump 0 sits between the header and record 0. **The
|
||||||
|
container's own start is the one branch point the cadence costs nothing at, and
|
||||||
|
it was the only one anybody had looked at.** The fix is one extra read of
|
||||||
|
7,168 B — **11.7 ms against 416, and no player here has an audio seek path** —
|
||||||
|
and the alternative is the cadence 32 called the worst one, F=1, which has no
|
||||||
|
group to enter off-boundary and gives back **12,288 B of RAM for +4.36 KB/s of
|
||||||
|
wire**. The deciding number is what one more SCSI command costs, and that is
|
||||||
|
hardware's (FINDINGS 70).
|
||||||
|
|
||||||
**Nothing outside-derived is committed here.** The scene graph is not
|
**Nothing outside-derived is committed here.** The scene graph is not
|
||||||
redistributable from this tree; it is regenerated from a reader's own clones
|
redistributable from this tree; it is regenerated from a reader's own clones
|
||||||
into gitignored `tmp/`, and `tools/import/scenegraph.py` is the single file in
|
into gitignored `tmp/`, and `tools/import/scenegraph.py` is the single file in
|
||||||
@@ -506,7 +535,9 @@ volume and compares every one of them**, then encodes the same window's audio
|
|||||||
and gates it against ffmpeg's decoder, then asks the emulated MSM6258 which of
|
and gates it against ffmpeg's decoder, then asks the emulated MSM6258 which of
|
||||||
sixteen decoders it is, then gates the DLXP2 container and every one of its
|
sixteen decoders it is, then gates the DLXP2 container and every one of its
|
||||||
axes, then measures the audio level off every stream of the game's own footage
|
axes, then measures the audio level off every stream of the game's own footage
|
||||||
and gates the disc's peak against the chip's clamp, then prints `ALL GREEN`.
|
and gates the disc's peak against the chip's clamp, then runs the refill climb
|
||||||
|
against the arcade's own 612 branch points with the second stream on the wire,
|
||||||
|
then prints `ALL GREEN`.
|
||||||
|
|
||||||
## Reproducing this
|
## Reproducing this
|
||||||
|
|
||||||
@@ -661,6 +692,12 @@ tools/analysis/ measurement scripts, numbered in the order they were written.
|
|||||||
a different sound from 687 in one), and what six gains cost on
|
a different sound from 687 in one), and what six gains cost on
|
||||||
windows drawn over the whole game. --gate asserts those
|
windows drawn over the whole game. --gate asserts those
|
||||||
numbers; --recover is the control that kills 66.3's mechanism.
|
numbers; --recover is the control that kills 66.3's mechanism.
|
||||||
|
36 puts 51.3's climb, 56.3's 612 real branch points and 32's
|
||||||
|
cadence in one place: what a SECOND CONSUMER does to the
|
||||||
|
climb, and what a seek into the middle of an audio group
|
||||||
|
costs. It reads only DLXSCENE1 and both containers' own
|
||||||
|
headers, and its one design assumption -- one container per
|
||||||
|
SCENE -- is printed in its own output.
|
||||||
buscost.py is the shared bus-cycle table. The per-block
|
buscost.py is the shared bus-cycle table. The per-block
|
||||||
constants live in tools/encoder/vq_hybrid.py and are imported,
|
constants live in tools/encoder/vq_hybrid.py and are imported,
|
||||||
never copied.
|
never copied.
|
||||||
|
|||||||
@@ -7529,3 +7529,183 @@ and was aimed one layer too late.
|
|||||||
sees, which is the right unit for a clamp question and the wrong one for a
|
sees, which is the right unit for a clamp question and the wrong one for a
|
||||||
mixing question.
|
mixing question.
|
||||||
* **Nothing was listened to.** Unchanged since 68.6.
|
* **Nothing was listened to.** Unchanged since 68.6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Findings — session 38 (2026-08-25)
|
||||||
|
|
||||||
|
## 70. The refill climb meets a branch point with sound on the wire — and the cadence turns out to have a third column (session 38)
|
||||||
|
|
||||||
|
ROADMAP P6, the last item in it and the oldest: 65.6 recorded that "the slack
|
||||||
|
table is here, but 51.3's refill climb with a second consumer through a real
|
||||||
|
branch point is not", and 67.6 repeated it word for word two sessions later.
|
||||||
|
`tools/analysis/36_branch_audio.py` is that run. It puts five things that have
|
||||||
|
never been in the same room together — 51.3's surplus model, 56.3's 612 real
|
||||||
|
branch points, 32's cadence arithmetic, the two containers, and the buffer
|
||||||
|
constants of the player 68 measured — and it needs no machine, because every
|
||||||
|
one of them is already a number.
|
||||||
|
|
||||||
|
python3 tools/analysis/36_branch_audio.py --kbps 451.4 488.0 513.2 600.0 \
|
||||||
|
--ring 256 512 # ~10 s
|
||||||
|
python3 tools/analysis/36_branch_audio.py --gate # the check.sh stage
|
||||||
|
|
||||||
|
### 70.1 THE HEADLINE, on the codec branch: audio is 1.7% of the wire and up to 3.30x of the climb
|
||||||
|
|
||||||
|
Slack is accumulated out of `pipe - wire` (51.3), and `pipe - wire` is a small
|
||||||
|
difference of two large numbers. A second consumer is therefore **not** priced
|
||||||
|
by its share of the wire, which is how every budget in this tree from FINDINGS 8
|
||||||
|
onward has quoted it. Gate container, the game's own 612 gaps:
|
||||||
|
|
||||||
|
| ring KB | pipe | ceiling | climb SILENT | SOUNDED | ×| under, silent | under, sounded |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 256 | 451.4 | 2 | 6.70 s | **22.11 s** | **3.30** | 482/612 (79%) | **603/612 (99%)** |
|
||||||
|
| 256 | 488.0 | 7 | 5.40 s | 6.44 s | 1.19 | 451/612 | 475/612 |
|
||||||
|
| 256 | 513.2 | 7 | 3.53 s | 3.95 s | 1.12 | 351/612 | 372/612 |
|
||||||
|
| 256 | 600.0 | 7 | 1.61 s | 1.69 s | 1.05 | 129/612 | 130/612 |
|
||||||
|
| 512 | 488.0 | 10 | 7.72 s | 9.19 s | 1.19 | 514/612 | 542/612 |
|
||||||
|
| 512 | 513.2 | 14 | 7.06 s | 7.89 s | 1.12 | 497/612 | 520/612 |
|
||||||
|
| 512 | 600.0 | 14 | 3.22 s | 3.38 s | 1.05 | 298/612 | 303/612 |
|
||||||
|
|
||||||
|
**7.63 KB/s of sound triples the climb at 451.4 and pushes the branch points
|
||||||
|
that arrive under it from 79% to 99% of the game.** Nothing about the audio got
|
||||||
|
bigger between those two columns; the quantity it is subtracted from got
|
||||||
|
smaller. 451.4 is not a corner case — it is the container's own zero-prefill
|
||||||
|
pipe (49.5), the rate at which the *silent* container is exactly deliverable.
|
||||||
|
**The rate at which a container is deliverable and the rate at which it can
|
||||||
|
absorb a branch were already different (51.4); with a second consumer the gap
|
||||||
|
between them is a factor of three.**
|
||||||
|
|
||||||
|
A **correction to 56.4**, and it is small enough to be worth stating precisely
|
||||||
|
so it is not mistaken for a bigger one: that table charged audio at
|
||||||
|
`ratectl.AUDIO_KBPS = 7.8`, which is 7,812.5 B/s written in decimal kB. In the
|
||||||
|
binary KB every other column uses it is **7.6294**, i.e. the placeholder was
|
||||||
|
**2.24% high** — 0.04% of the wire. Every 56.4 column moves in the flattering
|
||||||
|
direction by less than one part in six hundred. A placeholder that turns out to
|
||||||
|
be right is still a placeholder.
|
||||||
|
|
||||||
|
### 70.2 On the packed branch there is no climb at all, and that is the finding
|
||||||
|
|
||||||
|
The packed player puts a record **straight into GVRAM** (61, 62, 64.2): the
|
||||||
|
transfer *is* the paint, so there is no record buffer and the **video lookahead
|
||||||
|
is zero records**. 51.3's question has no answer there, because there is no
|
||||||
|
accumulator for play to fill.
|
||||||
|
|
||||||
|
* **56.4's alarming column does not apply to the DMAC-direct packed player** —
|
||||||
|
"most branch points arrive with less lookahead than the one before them" is
|
||||||
|
vacuous when *every frame* does.
|
||||||
|
* Its acceptance is a **per-frame deadline**: 49,664 B inside every slot. A pipe
|
||||||
|
that averages 589.6 KB/s over a second is a different claim, and that is why
|
||||||
|
B1's acceptance has to name the data-phase burst rate as well as the sustained
|
||||||
|
one (64.2).
|
||||||
|
* **The only consumer on that branch with any lookahead is the audio one**:
|
||||||
|
7,168 B a lump, three slots and a prefill of two as run (68.6), which is
|
||||||
|
**1.833 s of sound held against 0.000 s of picture.** The stream this project
|
||||||
|
spent four sessions adding is the better-buffered of the two.
|
||||||
|
|
||||||
|
For the CPU-painted variant (64.2 column B, 99,328 B, two record buffers, one
|
||||||
|
record of lookahead):
|
||||||
|
|
||||||
|
| pipe | climb SILENT | SOUNDED | × | under, silent | under, sounded |
|
||||||
|
|---:|---:|---:|---:|---:|---:|
|
||||||
|
| **589.6** | 6.38 s | **never** | ∞ | 473/612 | **612/612** |
|
||||||
|
| 600.0 | 2.69 s | 4.68 s | 1.74 | 263/612 | 421/612 |
|
||||||
|
| 650.0 | 0.71 s | 0.80 s | 1.13 | 43/612 | 49/612 |
|
||||||
|
| 700.0 | 0.41 s | 0.44 s | 1.07 | 33/612 | 33/612 |
|
||||||
|
|
||||||
|
**Read the first row.** 589.6 KB/s is the acceptance figure this project quotes
|
||||||
|
for the packed branch, and it is precisely the rate at which the sounded
|
||||||
|
container's surplus is **zero** — it plays, and it never accumulates a byte of
|
||||||
|
lookahead at any amount of play. The silent container at that same rate climbs
|
||||||
|
its one record in 6.38 s. **"Deliverable" and "able to survive a branch" are the
|
||||||
|
same rate only for a container with no second consumer in it.**
|
||||||
|
|
||||||
|
### 70.3 The cost nobody had counted: entering a group off-boundary
|
||||||
|
|
||||||
|
A DLXP2 group is `lump k, then F records` (dlxp.py), so **lump k lies at a lower
|
||||||
|
address than every record of its group except the first**. Read forward from
|
||||||
|
record *i* and the next lump to arrive is *k+1*, which carries frame *(k+1)F* —
|
||||||
|
so the frames from *i* to *(k+1)F−1* have picture and no sound. The missing
|
||||||
|
time is exactly `(F − i mod F) mod F` frames of **1/12 s**, and it is an exact
|
||||||
|
1/12 s: the chip's second is 15,625 samples and none of FINDINGS 54's
|
||||||
|
frame-clock remainder gets into it.
|
||||||
|
|
||||||
|
Measured on the **409 within-container seek targets of the arcade's own graph**,
|
||||||
|
at the shipped cadence F=11:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---:|
|
||||||
|
| mean silence entering the branch | **416.5 ms** |
|
||||||
|
| median / p90 / worst | 416.7 / 750.0 / **833.3 ms** |
|
||||||
|
| land on a group boundary and cost nothing | **36 of 409** |
|
||||||
|
|
||||||
|
The other **203 branch points — the scene changes — are free, and by
|
||||||
|
construction**: lump 0 sits at sector 1 and record 0 at 7,680, so a container's
|
||||||
|
first bytes are header, lump, record and a scene change reads them in one
|
||||||
|
forward pass. **The container's own start is the one branch point the cadence
|
||||||
|
costs nothing at, and it is the only one anybody had ever looked at.**
|
||||||
|
|
||||||
|
**The fix is a second read, and no player in this tree has one.** Lump *k* is up
|
||||||
|
to 503,808 B behind record *i*, so it cannot be picked up by reading early — it
|
||||||
|
is a separate command at a separate LBA, of 7,168 B, which at 600 KB/s is
|
||||||
|
**11.7 ms against a mean 416 ms of silence, 36x cheaper in time**. It costs one
|
||||||
|
more command per branch and **the command overhead is B1's and unmeasured**.
|
||||||
|
`src/player/packed.s` starts `PG_AK`/`PG_AKF` at lump 0 and has no audio seek
|
||||||
|
path at all.
|
||||||
|
|
||||||
|
### 70.4 So the cadence pick is reopened, on a column it did not have
|
||||||
|
|
||||||
|
32_audio_wire.py chose F=11 on two columns, padding and RAM. Here is the third,
|
||||||
|
measured on the game's own seek targets:
|
||||||
|
|
||||||
|
| F | A | lump B | padding | audio KB/s | RAM ×2 | mean sil | worst | free |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| **1** | 2 | 1,024 | 57.29% | 12.000 | **2,048** | **0.0** | **0.0** | **409/409** |
|
||||||
|
| 3 | 4 | 2,048 | 4.86% | 8.000 | 4,096 | 102.5 | 166.7 | 93/409 |
|
||||||
|
| 7 | 9 | 4,608 | 1.11% | 7.714 | 9,216 | 209.5 | 500.0 | 78/409 |
|
||||||
|
| **11** | 14 | 7,168 | **0.09%** | **7.636** | 14,336 | 416.5 | 833.3 | 36/409 |
|
||||||
|
| 12 | 16 | 8,192 | 4.86% | 8.000 | 16,384 | 519.4 | 916.7 | 34/409 |
|
||||||
|
|
||||||
|
**F=1 — the cadence 32_audio_wire called the worst one — has no group to enter
|
||||||
|
off-boundary at all**: no second read, no audio seek path, no silence at any
|
||||||
|
branch point, and 2,048 B of held lump instead of 14,336. It costs **+4.36 KB/s
|
||||||
|
of wire, 0.74% of the packed acceptance figure**, and it **gives back 12,288 B
|
||||||
|
of RAM** on the branch whose entire argument is that RAM is the resource it has
|
||||||
|
spare (64.2).
|
||||||
|
|
||||||
|
**This is a trade and not an error.** Padding is what F=11 minimises and padding
|
||||||
|
is not the only thing F sets. A player that gets its audio seek right is
|
||||||
|
indifferent between them; a player that does not pays a mean 416 ms of silence
|
||||||
|
at 409 of the game's 612 branch points. **Nothing here decides it — the
|
||||||
|
deciding number is the SCSI command overhead of the extra read, and that is
|
||||||
|
B1's.** F=7 is the middle if one is wanted: 1.11% padding, +0.08 KB/s over
|
||||||
|
F=11, 9,216 B held, and half the silence.
|
||||||
|
|
||||||
|
**And the content is not uniform mod F.** A uniform assumption puts the mean at
|
||||||
|
(F−1)/2 frames; the arcade's seek targets land where they land, and the measured
|
||||||
|
mean runs **0.67x to 1.23x** of uniform across the sweep — a design that assumed
|
||||||
|
uniform would be a quarter out at F=3. At the shipped F=11 the ratio is
|
||||||
|
**1.00x**, which is a coincidence and is reported as one.
|
||||||
|
|
||||||
|
### 70.5 What this does NOT establish
|
||||||
|
|
||||||
|
1. **No rate here is measured.** Every pipe column is a sensitivity (FINDINGS
|
||||||
|
50). **B1 — sustained *and* data-phase burst — is still the user's**, and
|
||||||
|
70.2's first row is a reminder that the two are not interchangeable.
|
||||||
|
2. **The frame index of a seek target is a DESIGN ASSUMPTION**, and it is the
|
||||||
|
one thing 70.3 and 70.4 rest on. DLXSCENE1 carries laserdisc-timeline
|
||||||
|
positions in ms; this design puts **one container per scene** (53, 55.1, and
|
||||||
|
56.3's 203 container changes), so the index is
|
||||||
|
`(target start − the scene's earliest start) × fps / 1000`. **One container
|
||||||
|
per *sequence* makes every seek land on frame 0, the group offset always
|
||||||
|
zero, and 70.3/70.4 empty.** Nothing else in the file changes.
|
||||||
|
3. **Nothing ran on a machine.** This is arithmetic over a scene table, two
|
||||||
|
containers and a player's own constants. **68's player has never seeked**,
|
||||||
|
and the audio seek path 70.3 asks for does not exist to be measured.
|
||||||
|
4. **The mechanical seek is still unmodelled** (51.7.5) and is charged on top of
|
||||||
|
every millisecond above.
|
||||||
|
5. **The silence is a container property, not a chip one.** What an MSM6258V
|
||||||
|
does when it is not fed — hold the last sample, or click — is a board
|
||||||
|
question and belongs with session 34's fifth hardware item.
|
||||||
|
6. **The 409/203 split is the graph's, not a container manifest's.** No scene
|
||||||
|
has been encoded but one; the mapping from a scene to a DLXP2 is a design
|
||||||
|
statement this tree has made and not yet built.
|
||||||
|
|||||||
+33
-4
@@ -172,6 +172,28 @@ through a real branch point** — and 69.4 adds two small unbuilt cases, a scene
|
|||||||
with no audio track (`00176`) and a scene whose audio is shorter than its video
|
with no audio track (`00176`) and a scene whose audio is shorter than its video
|
||||||
(`00199`).
|
(`00199`).
|
||||||
|
|
||||||
|
Amended end of session 38: **P6 IS CLOSED, AND THE LAST ITEM IN IT REOPENED THE
|
||||||
|
CADENCE (FINDINGS 70).** `tools/analysis/36_branch_audio.py` runs 51.3's climb
|
||||||
|
against 56.3's 612 real branch points with the second consumer on the wire. On
|
||||||
|
the codec branch **audio is 1.7% of the wire and up to 3.30x of the climb** —
|
||||||
|
6.70 s → 22.11 s at 451.4 KB/s, and 79% → 99% of the game's branch points
|
||||||
|
arriving under it — because slack accrues out of `pipe − wire` and that is a
|
||||||
|
small difference of two large numbers. On the packed branch **there is no climb
|
||||||
|
at all**: a record goes straight into GVRAM, video lookahead is zero records,
|
||||||
|
and the only consumer with any lookahead is the audio one (1.833 s of sound
|
||||||
|
against 0.000 s of picture). **At 589.6 KB/s — the acceptance figure — the
|
||||||
|
sounded packed container's surplus is exactly zero**, which is the sharpest
|
||||||
|
statement yet of why B1 must name a burst rate and not only a sustained one.
|
||||||
|
And a cost nobody had counted: a DLXP2 group is `lump k, then F records`, so a
|
||||||
|
seek into a group finds its audio BEHIND it — **mean 416.5 ms of silence at the
|
||||||
|
409 within-container branch points, worst 833.3 ms, 36 of 409 free**, while all
|
||||||
|
203 scene changes are free by construction. **The fix is one extra read of
|
||||||
|
7,168 B (11.7 ms against 416) and no player has an audio seek path**; the
|
||||||
|
alternative is F=1, which gives back 12,288 B of RAM for +4.36 KB/s of wire.
|
||||||
|
**The cadence pick is therefore reopened and the deciding number is B1's**: what
|
||||||
|
one extra SCSI command costs. **What is left of P6 is nothing** — 69.4's two
|
||||||
|
unbuilt cases and 70.3's seek path are player work, not open questions.
|
||||||
|
|
||||||
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
|
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
|
||||||
tree — a decision point, two outcomes, a death clip — with audio, streaming from
|
tree — a decision point, two outcomes, a death clip — with audio, streaming from
|
||||||
a real SCSI volume on a stock 2 MB machine, playable. That is the point at which
|
a real SCSI volume on a stock 2 MB machine, playable. That is the point at which
|
||||||
@@ -736,8 +758,10 @@ playing from disc on stock hardware.**
|
|||||||
**P6. Audio. P6a DONE, session 34 — FINDINGS 66.** ~~and it is the largest
|
**P6. Audio. P6a DONE, session 34 — FINDINGS 66.** ~~and it is the largest
|
||||||
unpriced risk left in the project.~~ Three quarters of P6 closed in session 33
|
unpriced risk left in the project.~~ Three quarters of P6 closed in session 33
|
||||||
and the fourth precondition — which decoder the chip runs — closed in 34, on the
|
and the fourth precondition — which decoder the chip runs — closed in 34, on the
|
||||||
machine, through the real DMA channel. **What is left of P6 is P6b (DLXP2, a
|
machine, through the real DMA channel. ~~**What is left of P6 is P6b (DLXP2, a
|
||||||
container with sound in it) and the refill climb with a second consumer.**
|
container with sound in it) and the refill climb with a second consumer.**~~
|
||||||
|
**P6 IS CLOSED end of session 38** — P6b in 35, P6c in 36, the level in 37 and
|
||||||
|
the refill climb with a second consumer in 38 (FINDINGS 70).
|
||||||
|
|
||||||
*(original framing, kept because every figure below is still the live one:)*
|
*(original framing, kept because every figure below is still the live one:)*
|
||||||
MSM6258 ADPCM, 15.6 kHz mono, **7.8 KB/s**. That figure is in `ratectl.py`'s
|
MSM6258 ADPCM, 15.6 kHz mono, **7.8 KB/s**. That figure is in `ratectl.py`'s
|
||||||
@@ -780,10 +804,15 @@ container's cadence is `F=11, A=14`** — 0.09% padding, 14,336 B held, 582.0
|
|||||||
57.3% of every audio sector. **The codec container pays zero padding**, because
|
57.3% of every audio sector. **The codec container pays zero padding**, because
|
||||||
it already has the index the packed one deleted (65.4).
|
it already has the index the packed one deleted (65.4).
|
||||||
|
|
||||||
**What is left in P6 is the fourth: the refill climb with a second consumer
|
~~**What is left in P6 is the fourth: the refill climb with a second consumer
|
||||||
through a real branch point** (51.3, 55.4). The slack table is in
|
through a real branch point** (51.3, 55.4). The slack table is in
|
||||||
`32_audio_wire.py` — at 582.0 KB/s exactly, silent breaks even and sounded
|
`32_audio_wire.py` — at 582.0 KB/s exactly, silent breaks even and sounded
|
||||||
starves — but a branch point has not been run with audio on the wire.
|
starves — but a branch point has not been run with audio on the wire.~~
|
||||||
|
**DONE, session 38 — FINDINGS 70**, and it produced three things the slack table
|
||||||
|
could not: the climb multiplier (up to **3.30x** for **1.7%** of the wire), the
|
||||||
|
fact that the packed branch has **no accumulator to climb**, and the group-entry
|
||||||
|
silence (**mean 416.5 ms** at 409 of the game's 612 branch points) that reopens
|
||||||
|
the cadence pick on a third column. `tools/analysis/36_branch_audio.py`.
|
||||||
|
|
||||||
**P6a. WHICH DELTA FORMULA DOES THE MSM6258 RUN? (new, session 33, FINDINGS
|
**P6a. WHICH DELTA FORMULA DOES THE MSM6258 RUN? (new, session 33, FINDINGS
|
||||||
65.2 — and it is a precondition, not a refinement.)** ffmpeg computes
|
65.2 — and it is a precondition, not a refinement.)** ffmpeg computes
|
||||||
|
|||||||
+103
@@ -1,3 +1,106 @@
|
|||||||
|
# Status & next-session handoff — end of session 38 (2026-08-25)
|
||||||
|
|
||||||
|
## Session 38: the climb meets a branch point with sound on it, and the cadence has a third column
|
||||||
|
|
||||||
|
**FINDINGS 70.** ROADMAP P6's last and oldest item, named by 65.6 and repeated
|
||||||
|
verbatim by 67.6: 51.3's refill climb with a second consumer, through a real
|
||||||
|
branch point. `tools/analysis/36_branch_audio.py` puts five things that had
|
||||||
|
never been in the same room together — 51.3's surplus model, 56.3's 612 real
|
||||||
|
branch points, 32's cadence arithmetic, both containers, and the buffer
|
||||||
|
constants of the player 68 measured. It needs no machine: all five were already
|
||||||
|
numbers.
|
||||||
|
|
||||||
|
**THE HEADLINE. Audio is 1.7% of the codec wire and up to 3.30x of the climb.**
|
||||||
|
Slack accrues out of `pipe − wire`, which is a small difference of two large
|
||||||
|
numbers, so a second consumer is *not* priced by its share of the wire — which
|
||||||
|
is how every budget in this tree since FINDINGS 8 has quoted it. At 451.4 KB/s
|
||||||
|
(the container's own zero-prefill pipe, 49.5) in a 256 KB ring the climb goes
|
||||||
|
**6.70 s → 22.11 s** and the branch points arriving under it go **482/612 (79%)
|
||||||
|
→ 603/612 (99%)**. At 488 it is 1.19x, at 600 it is 1.05x.
|
||||||
|
|
||||||
|
**AND ON THE PACKED BRANCH THERE IS NO CLIMB AT ALL.** The packed player puts a
|
||||||
|
record straight into GVRAM, so its **video lookahead is zero records** and
|
||||||
|
51.3's question has no answer there: 56.4's alarming column is vacuous when
|
||||||
|
every frame arrives with less lookahead than the one before it. Its acceptance
|
||||||
|
is a per-frame deadline, not an average, which is why B1 has to name a burst
|
||||||
|
rate. **The only consumer on that branch with any lookahead is the audio one** —
|
||||||
|
1.833 s of sound held against 0.000 s of picture. And on the CPU-painted variant
|
||||||
|
the acceptance figure itself reads differently: **at 589.6 KB/s the sounded
|
||||||
|
container's surplus is exactly zero**, so it plays and never accumulates a byte,
|
||||||
|
while the silent one climbs its record in 6.38 s.
|
||||||
|
|
||||||
|
**THE COST NOBODY HAD COUNTED.** A DLXP2 group is `lump k, then F records`, so
|
||||||
|
lump *k* is at a LOWER address than every record of its group but the first. A
|
||||||
|
seek to record *i* finds its audio behind it and the next lump ahead carries
|
||||||
|
frame *(k+1)F*. Measured on the **409 within-container seek targets of the
|
||||||
|
arcade's own graph** at the shipped F=11: **mean 416.5 ms of silence entering a
|
||||||
|
branch, worst 833.3 ms, and only 36 of 409 land on a group boundary.** The other
|
||||||
|
203 — the scene changes — are free by construction, because lump 0 sits between
|
||||||
|
the header and record 0. **The container's own start is the one branch point the
|
||||||
|
cadence costs nothing at, and it is the only one anybody had looked at.**
|
||||||
|
|
||||||
|
**SO THE CADENCE PICK IS REOPENED**, on a third column. F=1 — the cadence
|
||||||
|
32_audio_wire called the worst one — has no group to enter off-boundary, and it
|
||||||
|
gives back **12,288 B of RAM** for **+4.36 KB/s of wire (0.74%)**. The
|
||||||
|
alternative is a second read: lump *k* is up to 503,808 B behind, so it is a
|
||||||
|
separate command of 7,168 B — **11.7 ms against 416 ms, 36x cheaper in time** —
|
||||||
|
and `src/player/packed.s` has no audio seek path at all. **Nothing here decides
|
||||||
|
it; the deciding number is the extra command's overhead, and that is B1's.**
|
||||||
|
|
||||||
|
**What changed in the tree.** One new analysis file and one new ~10 s check.sh
|
||||||
|
stage that gates the STRUCTURE and deliberately not the milliseconds: audio
|
||||||
|
never shortens a climb, the shipped cadence strands most within-container
|
||||||
|
branches, F=1 strands none, and the lump read is an order of magnitude cheaper
|
||||||
|
than the silence it removes. No encoder, player or container byte moved.
|
||||||
|
|
||||||
|
## HANDOFF — start here
|
||||||
|
|
||||||
|
**THE TREE IS ALL GREEN**, session 38's stage included.
|
||||||
|
|
||||||
|
### The work, in the order it should be done
|
||||||
|
|
||||||
|
**1. THE AUDIO SEEK PATH (70.3), and it is now the cheapest thing that moves
|
||||||
|
something.** `PG_AK`/`PG_AKF` start at lump 0 and there is no way to enter a
|
||||||
|
group off-boundary. One extra read at `PG_ALBA0 + k*(F*PG_RECS + PG_CADA)` plus
|
||||||
|
a byte offset inside the lump is the whole of it, and until it exists the player
|
||||||
|
cannot branch with sound. It also turns 70.4's reopened cadence question back
|
||||||
|
into a non-question.
|
||||||
|
|
||||||
|
**2. THE BUFFERING DEPTH, which is still cheap.** Unchanged from session 37's
|
||||||
|
handoff: `PG_ANBUF` is 3 and `PG_APRE` is a mailbox. Two slots is one constant
|
||||||
|
and one run. 70.2 adds a reason to care — the audio buffer is the packed
|
||||||
|
branch's *only* buffer.
|
||||||
|
|
||||||
|
**3. THE SILENT-CLIP AND SHORT-AUDIO CASES (69.4).** Unchanged: a scene whose
|
||||||
|
audio is shorter than its frames (`00199`), and a scene with no audio track at
|
||||||
|
all (`00176`). Both are decided by what the lump count is computed from.
|
||||||
|
|
||||||
|
### What is still BLOCKED, so it is not picked up by mistake
|
||||||
|
|
||||||
|
**K4 — the packed player that is on screen — is conditional on B2**, a board
|
||||||
|
question. **E7, E4 and C1** are parked (61.8), and **P4a's wiring** is parked
|
||||||
|
with the ring K3 deleted.
|
||||||
|
|
||||||
|
**The hardware list is unchanged and is the user's**: B1 (sustained AND the
|
||||||
|
data-phase BURST rate, 64.2 — acceptance is **589.6 KB/s**, and 70.2 is the
|
||||||
|
sharpest statement yet of why the two rates are not interchangeable), B2
|
||||||
|
(blanking), B3 (`#EXREQ`), B4 (a byte write to a palette register), and session
|
||||||
|
34's fifth, play a known nibble stream on a real MSM6258V and record the line
|
||||||
|
out. **Session 38 adds a sixth question to B1 and it is small**: what one extra
|
||||||
|
SCSI command costs, because that is the number 70.4's cadence trade turns on.
|
||||||
|
|
||||||
|
### Reproducing this session
|
||||||
|
|
||||||
|
python3 tools/analysis/36_branch_audio.py --kbps 451.4 488.0 513.2 600.0 \
|
||||||
|
--ring 256 512 # ~10 s, the essay
|
||||||
|
python3 tools/analysis/36_branch_audio.py --gate # the check.sh stage
|
||||||
|
./tools/bench/check.sh # ALL GREEN
|
||||||
|
|
||||||
|
**WHAT IS NEXT.** The audio seek path, which is the first thing in this project
|
||||||
|
that the content has asked for rather than the hardware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Status & next-session handoff — end of session 37 (2026-08-25)
|
# Status & next-session handoff — end of session 37 (2026-08-25)
|
||||||
|
|
||||||
## Session 37: the audio level is measured off the whole disc, and the headroom is not worth buying
|
## Session 37: the audio level is measured off the whole disc, and the headroom is not worth buying
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""51.3's REFILL CLIMB WITH A SECOND CONSUMER, THROUGH A REAL BRANCH POINT.
|
||||||
|
|
||||||
|
python3 tools/analysis/36_branch_audio.py --kbps R [R ...]
|
||||||
|
[--ring KB ...] [--gate]
|
||||||
|
|
||||||
|
The oldest open item in ROADMAP P6, named by FINDINGS 65.6 and again by 67.6:
|
||||||
|
|
||||||
|
"the slack table is here, but 51.3's refill climb with a second consumer
|
||||||
|
through a real branch point is not."
|
||||||
|
|
||||||
|
Everything it needs already exists and none of it has ever been put in the same
|
||||||
|
room:
|
||||||
|
|
||||||
|
* 51.3 -- slack is ACCUMULATED out of `pipe - wire`, at `pipe - wire` bytes a
|
||||||
|
second, and a seek spends all of it. `tools/analysis/20_seek_slack.py`.
|
||||||
|
* 56.3 -- where the branch points ARE: 612 distinct transitions into a seek
|
||||||
|
over the arcade's own graph, worst gap 0.000 s, median 3.473 s.
|
||||||
|
`tools/analysis/25_scene_graph.py`, reading only DLXSCENE1.
|
||||||
|
* 56.4 -- the climb against that distribution, for the codec container. It
|
||||||
|
charged audio as `ratectl.AUDIO_KBPS`, a flat 7.8 KB/s placeholder that
|
||||||
|
predates any of the audio work.
|
||||||
|
* 65.3/67.1 -- what a second consumer ACTUALLY costs a container: a fixed
|
||||||
|
cadence of F frames per A sectors, because a packed record's address is
|
||||||
|
arithmetic and cannot be an index. `tools/analysis/32_audio_wire.py`.
|
||||||
|
* 68 -- the player that holds both streams at once, and its buffers.
|
||||||
|
|
||||||
|
Three questions, and the tree has never asked any of them:
|
||||||
|
|
||||||
|
1. What does the SECOND CONSUMER do to the climb? Not to the wire -- 32
|
||||||
|
answered that and it is 1.3% -- but to `pipe - wire`, which is a small
|
||||||
|
difference of two large numbers and is the thing the climb is made of.
|
||||||
|
|
||||||
|
2. What is the climb on the PACKED branch? This is the branch the player
|
||||||
|
runs (68) and the one B1's acceptance is written against.
|
||||||
|
|
||||||
|
3. What does the CADENCE do at a branch point? A group is `lump k, then F
|
||||||
|
records`, so lump k sits at a LOWER address than every record in its group
|
||||||
|
but the first. A seek to record i lands inside a group whose audio is
|
||||||
|
BEHIND it. Nobody has ever priced entering a group off-boundary, and the
|
||||||
|
game's own seek targets say how often it happens.
|
||||||
|
|
||||||
|
THE FRAME INDEX OF A SEEK TARGET IS A DESIGN ASSUMPTION AND IS LABELLED ONE.
|
||||||
|
DLXSCENE1 carries positions on the laserdisc timeline in ms. This tree's design
|
||||||
|
puts ONE CONTAINER PER SCENE -- 53 and 55.1 charge a scene change 6,164 header
|
||||||
|
bytes, and 56.3 counts 203 of the 612 transitions as container changes for
|
||||||
|
exactly that reason -- so a seek target's frame index inside its container is
|
||||||
|
`(target start - the scene's own earliest start) * fps / 1000`. If the design
|
||||||
|
ever puts one container per SEQUENCE instead, every seek lands on frame 0, the
|
||||||
|
group offset is always zero and section 3 collapses to nothing. That is the
|
||||||
|
assumption, said out loud, in the one place the answer depends on it.
|
||||||
|
"""
|
||||||
|
import sys, os, json, argparse, importlib.util
|
||||||
|
|
||||||
|
sys.path.insert(0, "tools/encoder")
|
||||||
|
sys.path.insert(0, "tools/analysis")
|
||||||
|
|
||||||
|
TABLE = os.environ.get("DLX_SCENEGRAPH", "tmp/scenegraph.json")
|
||||||
|
FPS = 12
|
||||||
|
CHIP_HZ = 15625.0 # MSM6258V, 8 MHz / 512 (FINDINGS 32, 65)
|
||||||
|
AU_BPS = CHIP_HZ / 2 # 4 bits a sample, two samples to a byte
|
||||||
|
AU_FRAME = AU_BPS / FPS # 651.0416... B a slot, and the dots are 65.3
|
||||||
|
SECTOR = 512
|
||||||
|
|
||||||
|
|
||||||
|
def load(path, name):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
m = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(m)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def cadence(F):
|
||||||
|
"""Best A for this F: the fewest whole sectors that hold F frames of chip."""
|
||||||
|
num, den = int(round(AU_BPS * 2)), 2 * SECTOR * FPS
|
||||||
|
A = -(-(num * F) // den) # ceil(F * bytes/sector)
|
||||||
|
return A, A * SECTOR
|
||||||
|
|
||||||
|
|
||||||
|
def climb_s(records, rec_b, pipe_kbps, wire_kbps):
|
||||||
|
"""Seconds of play to accumulate `records` records of lookahead (51.3)."""
|
||||||
|
surplus = (pipe_kbps - wire_kbps) * 1024.0
|
||||||
|
return float("inf") if surplus <= 0 else records * rec_b / surplus
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- the branches
|
||||||
|
|
||||||
|
def branch_points(doc, nodes, sg):
|
||||||
|
"""The 612 transitions into a seek, plus each one's target frame index.
|
||||||
|
|
||||||
|
Returns (gaps_s, within, changes). `within` entries carry the frame index
|
||||||
|
the seek lands on inside its container; `changes` are scene changes, which
|
||||||
|
land on frame 0 of a new one.
|
||||||
|
"""
|
||||||
|
gaps, _ = sg.worst_gap(nodes)
|
||||||
|
play = [g for g in gaps if g[5] != "attract_mode"]
|
||||||
|
scene_start = {}
|
||||||
|
for scene, seqs in doc["scenes"].items():
|
||||||
|
st = [s["start_ms"] for s in seqs.values() if s["start_ms"] >= 0]
|
||||||
|
scene_start[scene] = min(st) if st else None
|
||||||
|
|
||||||
|
within, changes = [], []
|
||||||
|
for g, src, tgt, kind, ends, scene in play:
|
||||||
|
if ends:
|
||||||
|
changes.append((g / 1000.0, src))
|
||||||
|
continue
|
||||||
|
n = nodes.get(f"{scene}.{tgt}")
|
||||||
|
if n is None or not n.seeks or scene_start[scene] is None:
|
||||||
|
continue
|
||||||
|
i = int(round((n.start - scene_start[scene]) / 1000.0 * FPS))
|
||||||
|
within.append((g / 1000.0, max(0, i), f"{scene}.{tgt}"))
|
||||||
|
return sorted(g[0] / 1000.0 for g in play), within, changes
|
||||||
|
|
||||||
|
|
||||||
|
def silences(within, F):
|
||||||
|
"""Ms of silence entering each branch's group off-boundary, at cadence F.
|
||||||
|
|
||||||
|
A group is `lump k, then F records`. Seek to record i, take the next lump
|
||||||
|
that lies AHEAD of the read point -- lump k+1, which arrives at frame
|
||||||
|
(k+1)*F -- and the frames from i to (k+1)*F-1 have no audio. The chip's
|
||||||
|
second is a real second (15,625 samples, 2 to a byte), so the missing
|
||||||
|
time is exactly `(F - i mod F) mod F` frames of 1/12 s and none of
|
||||||
|
FINDINGS 54's frame-clock remainder gets into it.
|
||||||
|
"""
|
||||||
|
return sorted(((F - (i % F)) % F) / FPS * 1000.0 for _, i, _ in within)
|
||||||
|
|
||||||
|
|
||||||
|
def pct(xs, p):
|
||||||
|
return xs[min(len(xs) - 1, int(p * len(xs)))] if xs else float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--kbps", type=float, nargs="+",
|
||||||
|
help="delivered pipe rates, KB/s. REQUIRED (FINDINGS 50) "
|
||||||
|
"unless --gate, which carries its own.")
|
||||||
|
ap.add_argument("--ring", type=float, nargs="+", default=[256, 512])
|
||||||
|
ap.add_argument("--pkbps", type=float, nargs="+",
|
||||||
|
default=[589.6, 600.0, 650.0, 700.0, 900.0],
|
||||||
|
help="pipe rates for the PACKED table. Its own list, "
|
||||||
|
"because a packed record is 1.3x the codec's and "
|
||||||
|
"every rate that serves one starves the other.")
|
||||||
|
ap.add_argument("--table", default=TABLE)
|
||||||
|
ap.add_argument("--packed", default="tmp/packed_singe.dlxp")
|
||||||
|
ap.add_argument("--codec", default="tmp/rc_fr_singe_scsi_span.dlx")
|
||||||
|
ap.add_argument("--fsweep", type=int, default=12,
|
||||||
|
help="highest cadence F in the pick table")
|
||||||
|
ap.add_argument("--gate", action="store_true",
|
||||||
|
help="check.sh mode: fixed rates, and assert the structural "
|
||||||
|
"results rather than print the essay")
|
||||||
|
a = ap.parse_args()
|
||||||
|
if a.gate and not a.kbps:
|
||||||
|
a.kbps = [451.4, 488.0, 600.0]
|
||||||
|
if not a.kbps:
|
||||||
|
ap.error("--kbps is required and has no default (FINDINGS 50)")
|
||||||
|
|
||||||
|
if not os.path.exists(a.table):
|
||||||
|
print(f"no scene table at {a.table} -- run:\n"
|
||||||
|
f" python3 tools/import/scenegraph.py")
|
||||||
|
return 2
|
||||||
|
doc = json.load(open(a.table))
|
||||||
|
if doc.get("format") != "DLXSCENE1":
|
||||||
|
print(f"{a.table}: not a DLXSCENE1 table")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
sg = load("tools/analysis/25_scene_graph.py", "scene_graph")
|
||||||
|
ss = load("tools/analysis/20_seek_slack.py", "seek_slack")
|
||||||
|
from dlxp import DLXP
|
||||||
|
import ratectl as RC
|
||||||
|
|
||||||
|
nodes = sg.build_graph(doc["scenes"])
|
||||||
|
gaps, within, changes = branch_points(doc, nodes, sg)
|
||||||
|
med = gaps[len(gaps) // 2]
|
||||||
|
|
||||||
|
d = DLXP(a.packed)
|
||||||
|
P_REC = d.rec_bytes
|
||||||
|
P_VID = P_REC * FPS / 1024
|
||||||
|
F0, A0 = d.cad_f, d.cad_a
|
||||||
|
P_AUD = A0 * SECTOR / F0 * FPS / 1024
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
=== WHAT EACH BRANCH ACTUALLY HOLDS ====================================
|
||||||
|
51.3's climb is a statement about an ACCUMULATOR. The two branches of this
|
||||||
|
project do not have the same one, and one of them does not have one at all.
|
||||||
|
|
||||||
|
codec ({os.path.basename(a.codec)})
|
||||||
|
a {a.ring[0]:.0f} KB ring of variable records with an index in front of it;
|
||||||
|
lookahead is whole records and the ceiling is what 20_seek_slack.py
|
||||||
|
simulates.
|
||||||
|
packed ({os.path.basename(a.packed)})
|
||||||
|
a record is {P_REC:,} B of literal picture and the channel puts it
|
||||||
|
STRAIGHT INTO GVRAM (FINDINGS 61, 62, 64). There is no record
|
||||||
|
buffer, so the VIDEO lookahead is ZERO records and there is nothing
|
||||||
|
to climb. The only consumer on that branch with any lookahead at
|
||||||
|
all is the AUDIO one: {d.cad_a * SECTOR:,} B a lump, PG_ANBUF={3} slots and
|
||||||
|
PG_APRE prefilled at {2} for the run 68 measured (it is a mailbox,
|
||||||
|
not a constant: src/player/packed.s, 68.6).
|
||||||
|
That is {2 * F0 / FPS:.3f} s of sound held against {0.0:.3f} s of picture.
|
||||||
|
|
||||||
|
The CPU-painted packed variant (64.2's column B) is the one that holds two
|
||||||
|
record buffers and 99,328 B, and it is the only packed configuration the
|
||||||
|
word "climb" applies to. Both are priced below.
|
||||||
|
|
||||||
|
=== THE BRANCH POINTS, OUT OF THE ARCADE'S OWN GRAPH ====================
|
||||||
|
{len(gaps)} transitions into a seek (attract mode excluded, 56.3)
|
||||||
|
worst {gaps[0]:.3f} s p10 {pct(gaps,.10):.3f} median {med:.3f} p90 {pct(gaps,.90):.3f}
|
||||||
|
{len(changes)} of them END THE SCENE and are container changes;
|
||||||
|
{len(within)} land INSIDE a container, at frame indices 0..{max(i for _, i, _ in within)}""")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 1
|
||||||
|
dc, rec = ss.records(a.codec)
|
||||||
|
C_REC = float(rec.mean())
|
||||||
|
C_VID = C_REC * FPS / 1024
|
||||||
|
print(f"""
|
||||||
|
=== 1. THE CLIMB WITH THE SECOND CONSUMER (the codec branch) ============
|
||||||
|
Audio is {AU_BPS/1024:.3f} KB/s and this container pays no padding for it (65.4: it
|
||||||
|
already has an index and already has variable records). Against a video wire
|
||||||
|
of {C_VID:.1f} KB/s that is {100*(AU_BPS/1024)/C_VID:+.2f}% -- and the climb is not built out of the wire,
|
||||||
|
it is built out of `pipe - wire`, so that is not the number that matters.
|
||||||
|
|
||||||
|
{'ring':>5} {'pipe':>7} {'ceil':>5} {'climb SILENT':>13} {'SOUNDED':>9} {'x':>6}"""
|
||||||
|
f" {'under, silent':>13} {'under, sounded':>14}")
|
||||||
|
tab1 = []
|
||||||
|
for ring_kb in a.ring:
|
||||||
|
ring = int(ring_kb * 1024)
|
||||||
|
for kbps in a.kbps:
|
||||||
|
fill = (kbps - AU_BPS / 1024) * 1024 / FPS
|
||||||
|
lo, hi, ring_ref, rate_ref = ss.paced_sim(rec, ring, fill)
|
||||||
|
ceil = int(hi.max())
|
||||||
|
cs = climb_s(ceil, C_REC, kbps, C_VID)
|
||||||
|
ca = climb_s(ceil, C_REC, kbps, C_VID + AU_BPS / 1024)
|
||||||
|
us = sum(1 for g in gaps if g < cs)
|
||||||
|
ua = sum(1 for g in gaps if g < ca)
|
||||||
|
ratio = ca / cs if cs not in (0.0, float("inf")) else float("inf")
|
||||||
|
tab1.append((ring_kb, kbps, ceil, cs, ca, ratio, us, ua))
|
||||||
|
print(f" {ring_kb:5.0f} {kbps:7.1f} {ceil:5d} {cs:12.2f}s "
|
||||||
|
f"{ca:8.2f}s {ratio:5.2f}x {f'{us}/{len(gaps)}':>13} "
|
||||||
|
f"{f'{ua}/{len(gaps)}':>14}")
|
||||||
|
|
||||||
|
worst = max(tab1, key=lambda r: r[5])
|
||||||
|
print(f"""
|
||||||
|
THE SECOND CONSUMER IS {100*(AU_BPS/1024)/C_VID:.1f}% OF THE WIRE AND UP TO {worst[5]:.2f}x OF THE CLIMB.
|
||||||
|
At {worst[1]:.1f} KB/s in a {worst[0]:.0f} KB ring the climb goes {worst[3]:.2f} s -> {worst[4]:.2f} s and the
|
||||||
|
branch points that arrive under it go {worst[6]}/{len(gaps)} -> {worst[7]}/{len(gaps)}
|
||||||
|
({100*worst[6]/len(gaps):.0f}% -> {100*worst[7]/len(gaps):.0f}%). Nothing about audio got bigger; the
|
||||||
|
DIFFERENCE it is subtracted from got smaller, and the climb is made of the
|
||||||
|
difference. This is why 51.4's rate/ring distinction matters more with a
|
||||||
|
second consumer than without one, and why quoting audio as a share of the
|
||||||
|
wire (32, and every budget before it) understates it at every rate close to
|
||||||
|
the wire.
|
||||||
|
|
||||||
|
A CORRECTION TO 56.4, and it is small: that table charged audio at
|
||||||
|
ratectl.AUDIO_KBPS = {RC.AUDIO_KBPS} KB/s, which is {AU_BPS:.1f} B/s expressed in decimal
|
||||||
|
kB (21_iplrom_dmac.py says so). In binary KB the figure is {AU_BPS/1024:.4f}, i.e.
|
||||||
|
{100*(RC.AUDIO_KBPS-AU_BPS/1024)/(AU_BPS/1024):+.2f}%. Every column of 56.4 moves in the flattering direction by
|
||||||
|
less than one part in six hundred of the wire. It is recorded because a
|
||||||
|
placeholder that turns out to be right is still a placeholder.""")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 2
|
||||||
|
print(f"""
|
||||||
|
=== 2. THE PACKED BRANCH: THERE IS NO CLIMB, AND THAT IS THE FINDING ====
|
||||||
|
video {P_VID:.1f} + audio {P_AUD:.4f} (F={F0}, A={A0}, {A0*SECTOR:,} B a lump) = {P_VID+P_AUD:.1f} KB/s,
|
||||||
|
which is B1's acceptance figure and is where it comes from.
|
||||||
|
|
||||||
|
DMAC-direct (what src/player/packed.s runs, FINDINGS 64/68):
|
||||||
|
video lookahead 0 records. The climb does not exist, the ceiling does not
|
||||||
|
exist, and the {len(gaps)} gaps buy it NOTHING -- there is no accumulator for
|
||||||
|
play to fill. Its acceptance is a PER-FRAME deadline: {P_REC:,} B must land
|
||||||
|
inside every slot, and a rate that averages {P_VID+P_AUD:.1f} KB/s over a second is
|
||||||
|
not the same claim. 56.4's alarming column -- most branch points arrive
|
||||||
|
with less lookahead than the one before them -- does not apply to it,
|
||||||
|
because every frame arrives with less lookahead than the one before it.
|
||||||
|
|
||||||
|
CPU-painted (64.2 column B, 99,328 B, two record buffers -> 1 record of
|
||||||
|
lookahead):
|
||||||
|
{'pipe':>7} {'climb SILENT':>13} {'SOUNDED':>9} {'x':>6} {'under, silent':>13} {'under, sounded':>14}""")
|
||||||
|
tab2 = []
|
||||||
|
def secs(x, w):
|
||||||
|
return f"{x:{w}.2f}s" if x != float("inf") else f"{'never':>{w+1}}"
|
||||||
|
for kbps in a.pkbps:
|
||||||
|
cs = climb_s(1, P_REC, kbps, P_VID)
|
||||||
|
ca = climb_s(1, P_REC, kbps, P_VID + P_AUD)
|
||||||
|
us = sum(1 for g in gaps if g < cs)
|
||||||
|
ua = sum(1 for g in gaps if g < ca)
|
||||||
|
r = ca / cs if cs not in (0.0, float("inf")) else float("inf")
|
||||||
|
tab2.append((kbps, cs, ca, r, us, ua))
|
||||||
|
print(f" {kbps:7.1f} {secs(cs,12)} {secs(ca,8)} "
|
||||||
|
f"{(f'{r:5.2f}x' if r != float('inf') else ' inf ')} "
|
||||||
|
f"{f'{us}/{len(gaps)}':>13} {f'{ua}/{len(gaps)}':>14}")
|
||||||
|
print(f" -- and EVERY rate in section 1's table is below {P_VID:.1f} KB/s, so "
|
||||||
|
f"none of\n them serves this container at all.")
|
||||||
|
print(f"""
|
||||||
|
READ THE FIRST ROW. At {P_VID+P_AUD:.1f} KB/s -- the acceptance figure this project
|
||||||
|
quotes -- the SILENT container still climbs its one record in {tab2[0][1]:.2f} s and
|
||||||
|
the SOUNDED one NEVER DOES, because {P_VID+P_AUD:.1f} is where its surplus is exactly
|
||||||
|
zero. The acceptance figure is the rate at which the sounded container has
|
||||||
|
no lookahead at any amount of play, which is a different thing from the rate
|
||||||
|
at which it plays.
|
||||||
|
|
||||||
|
A packed record is 1.3x the codec's mean record and the packed wire is 1.3x
|
||||||
|
the codec's, so a rate that is generous to one is tight for the other and the
|
||||||
|
same audio debit costs the packed climb more. ONE RECORD of lookahead is
|
||||||
|
1/12 s of tolerance and it takes seconds of play to earn.
|
||||||
|
|
||||||
|
THE TWO BRANCHES DIFFER HERE ON A COLUMN THAT IS NOT CLOCKS, which is the
|
||||||
|
third time (61.9, 64.2, and this). The packed branch spent its ring to
|
||||||
|
delete a decoder; what it bought with the RAM is a player with no tolerance
|
||||||
|
for a slow record at ANY time, not merely after a branch.""")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 3
|
||||||
|
sil = silences(within, F0)
|
||||||
|
free = sum(1 for x in sil if x == 0.0)
|
||||||
|
P_PIPE = a.pkbps[1] if len(a.pkbps) > 1 else a.pkbps[0]
|
||||||
|
lump_ms = A0 * SECTOR / (P_PIPE * 1024) * 1000
|
||||||
|
behind = (F0 - 1) * P_REC + A0 * SECTOR
|
||||||
|
print(f"""
|
||||||
|
=== 3. THE COST NOBODY HAD COUNTED: entering a group off-boundary =======
|
||||||
|
A DLXP2 group is `lump k, then F records` (dlxp.py), so lump k is at a LOWER
|
||||||
|
address than every record of its group except the first. Reading forward from
|
||||||
|
record i, the next lump to arrive is k+1, and it carries frame (k+1)*F. The
|
||||||
|
frames from i to (k+1)*F-1 therefore have picture and no sound.
|
||||||
|
|
||||||
|
Measured on the {len(within)} within-container seek targets of the arcade's own
|
||||||
|
graph, at the shipped cadence F={F0}:
|
||||||
|
|
||||||
|
mean {sum(sil)/len(sil):7.1f} ms of silence entering the branch
|
||||||
|
median {pct(sil,.50):7.1f} p90 {pct(sil,.90):7.1f} worst {sil[-1]:7.1f}
|
||||||
|
free {free}/{len(within)} land on a group boundary and cost nothing
|
||||||
|
|
||||||
|
The other {len(changes)} branch points -- the scene changes -- are FREE, and by
|
||||||
|
construction: lump 0 sits at sector 1 and record 0 at {d.off_frm:,}, so a
|
||||||
|
container's own first bytes are header, lump, record and a scene change reads
|
||||||
|
them in one forward pass. **The container's start is the one branch point
|
||||||
|
the cadence costs nothing at, and it is the only one anybody had looked at.**
|
||||||
|
|
||||||
|
THE FIX IS A SECOND READ AND NOBODY HAS ONE. Lump k is {behind:,} B behind
|
||||||
|
record i at worst, so it cannot be picked up by reading early -- it is a
|
||||||
|
separate command at a separate LBA, of {A0*SECTOR:,} B, which at {P_PIPE:.1f} KB/s is
|
||||||
|
{lump_ms:.1f} ms against a mean {sum(sil)/len(sil):.0f} ms of silence -- {sum(sil)/len(sil)/lump_ms:.0f}x cheaper in TIME,
|
||||||
|
one more command per branch, and the command overhead is B1's and unmeasured.
|
||||||
|
src/player/packed.s starts PG_AK and PG_AKF at lump 0 and has no audio seek
|
||||||
|
path at all; the player that branches needs one.
|
||||||
|
|
||||||
|
=== 4. THE CADENCE PICK, WITH THE THIRD COLUMN IT DID NOT HAVE ==========
|
||||||
|
32_audio_wire.py chose F={F0} on two columns, padding and RAM. Here is the same
|
||||||
|
sweep with the branch column, measured on the game's own seek targets rather
|
||||||
|
than assumed uniform:
|
||||||
|
|
||||||
|
{'F':>3} {'A':>3} {'lump B':>8} {'pad%':>7} {'aud KB/s':>9} {'RAM x2':>8} {'mean sil':>9} {'p90':>8} {'worst':>8} {'free':>10} {'vs uniform':>11}""")
|
||||||
|
for F in range(1, a.fsweep + 1):
|
||||||
|
A, lump = cadence(F)
|
||||||
|
need = F * AU_FRAME
|
||||||
|
s = silences(within, F)
|
||||||
|
uni = (F - 1) / 2 / FPS * 1000.0
|
||||||
|
mean = sum(s) / len(s)
|
||||||
|
mark = " <- shipped" if F == F0 else ""
|
||||||
|
print(f" {F:3d} {A:3d} {lump:8,} {100*(lump-need)/need:6.2f}% "
|
||||||
|
f"{lump/F*FPS/1024:8.3f} {2*lump:8,} {mean:8.1f} "
|
||||||
|
f"{pct(s,.90):8.1f} {s[-1]:8.1f} "
|
||||||
|
f"{f'{sum(1 for x in s if x == 0)}/{len(s)}':>10} "
|
||||||
|
f"{(mean/uni if uni else 1.0):10.2f}x{mark}")
|
||||||
|
|
||||||
|
ratios = []
|
||||||
|
for F in range(2, a.fsweep + 1):
|
||||||
|
sF_ = silences(within, F)
|
||||||
|
ratios.append((sum(sF_) / len(sF_)) / ((F - 1) / 2 / FPS * 1000.0))
|
||||||
|
min_r, max_r = min(ratios), max(ratios)
|
||||||
|
A1, l1 = cadence(1)
|
||||||
|
AF, lF = cadence(F0)
|
||||||
|
s1, sF = silences(within, 1), silences(within, F0)
|
||||||
|
print(f"""
|
||||||
|
F=1 -- "one lump a record", the cadence 32 called THE WORST ONE -- has no
|
||||||
|
group to enter off-boundary, no second read, no audio seek path and 2,048 B
|
||||||
|
of held lump instead of {2*lF:,}. It costs {l1/1*FPS/1024 - lF/F0*FPS/1024:+.3f} KB/s of wire, which is
|
||||||
|
{100*(l1/1*FPS/1024 - lF/F0*FPS/1024)/(P_VID+P_AUD):+.2f}% of the packed acceptance figure, and it BUYS BACK {2*lF-2*l1:,} B
|
||||||
|
of RAM on the branch whose whole argument is that RAM is what it has spare.
|
||||||
|
|
||||||
|
THE PICK IS THEREFORE REOPENED, and it is a real trade rather than an error:
|
||||||
|
padding is what F={F0} minimises and padding is not the only thing F sets.
|
||||||
|
A player that gets its audio seek right is indifferent; a player that does
|
||||||
|
not pays a mean {sum(sF)/len(sF):.0f} ms of silence at {len(within)} of the game's {len(gaps)} branch
|
||||||
|
points. Nothing here decides it -- the deciding number is the SCSI command
|
||||||
|
overhead of the extra read, and that is B1's.
|
||||||
|
|
||||||
|
AND THE CONTENT IS NOT UNIFORM MOD F. A uniform assumption would put the
|
||||||
|
mean at (F-1)/2 frames; the arcade's seek targets land where they land, and
|
||||||
|
the ratio column above runs {min_r:.2f}x..{max_r:.2f}x over the sweep, so a design
|
||||||
|
that assumed uniform would be out by a quarter at F=3. At the shipped F={F0} it is
|
||||||
|
{(sum(sF)/len(sF))/((F0-1)/2/FPS*1000):.2f}x, which is a coincidence and is reported as one.
|
||||||
|
|
||||||
|
=== 5. WHAT THIS DOES NOT ESTABLISH ====================================
|
||||||
|
1. NO RATE HERE IS MEASURED. Every pipe column is a sensitivity (FINDINGS
|
||||||
|
50), and B1 -- sustained AND data-phase burst -- is still the user's.
|
||||||
|
2. THE FRAME INDEX OF A SEEK TARGET IS A DESIGN ASSUMPTION. One container
|
||||||
|
per SCENE (53, 55.1, 56.3). One container per SEQUENCE makes section 3
|
||||||
|
zero and section 4 moot; nothing else in the file changes.
|
||||||
|
3. NOTHING RAN ON THE MACHINE. This is arithmetic over a scene table, two
|
||||||
|
containers and a player's own constants. 68's player has never seeked.
|
||||||
|
4. THE MECHANICAL SEEK IS STILL UNMODELLED (51.7.5) and is charged on top of
|
||||||
|
every millisecond here.
|
||||||
|
5. THE SILENCE IS A CONTAINER PROPERTY, NOT A CHIP ONE. What the MSM6258
|
||||||
|
does when it is not fed -- hold the last sample, or click -- is a board
|
||||||
|
question and belongs with session 34's fifth hardware item.""")
|
||||||
|
|
||||||
|
if a.gate:
|
||||||
|
# Structural assertions. Not the milliseconds -- those move with the
|
||||||
|
# scene table -- but the ORDER and the SIGNS, which are the finding.
|
||||||
|
ok = True
|
||||||
|
|
||||||
|
def check(cond, msg):
|
||||||
|
nonlocal ok
|
||||||
|
print(f" {'OK ' if cond else 'FAIL'} {msg}")
|
||||||
|
ok = ok and bool(cond)
|
||||||
|
|
||||||
|
print("\n=== GATE ===============================================")
|
||||||
|
check(len(gaps) == 612, f"612 transitions into a seek, got {len(gaps)}")
|
||||||
|
check(len(within) + len(changes) == len(gaps),
|
||||||
|
f"{len(within)} within + {len(changes)} scene changes = {len(gaps)}")
|
||||||
|
check(all(r[5] >= 1.0 for r in tab1),
|
||||||
|
"audio never SHORTENS the codec climb")
|
||||||
|
check(max(r[5] for r in tab1) > 1.5,
|
||||||
|
f"and at some rate it more than 1.5x's it "
|
||||||
|
f"({max(r[5] for r in tab1):.2f}x)")
|
||||||
|
check(all(r[7] >= r[6] for r in tab1),
|
||||||
|
"and never lowers the count of branch points under the climb")
|
||||||
|
check(free < len(within) // 2,
|
||||||
|
f"most within-container branches enter a group off-boundary "
|
||||||
|
f"({len(within)-free}/{len(within)})")
|
||||||
|
check(silences(within, 1) == [0.0] * len(within),
|
||||||
|
"F=1 has no off-boundary case at all")
|
||||||
|
check(sum(sil) / len(sil) > 10 * lump_ms,
|
||||||
|
f"the silence F={F0} costs is >10x the lump read that removes it "
|
||||||
|
f"({sum(sil)/len(sil):.0f} ms vs {lump_ms:.1f} ms)")
|
||||||
|
print(" " + ("BRANCH-AUDIO GATE GREEN" if ok else "BRANCH-AUDIO GATE RED"))
|
||||||
|
return 0 if ok else 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -846,4 +846,27 @@ python3 tools/analysis/35_audio_level.py --gate > tmp/audio_level.log 2>&1 \
|
|||||||
grep -aE "DISC PEAK|LOUDEST PASSAGE|THE CLAMP|LEVEL GATE|NO AUDIO TRACK|^ 1\.0000|^ 0\.5" \
|
grep -aE "DISC PEAK|LOUDEST PASSAGE|THE CLAMP|LEVEL GATE|NO AUDIO TRACK|^ 1\.0000|^ 0\.5" \
|
||||||
tmp/audio_level.log
|
tmp/audio_level.log
|
||||||
|
|
||||||
|
echo "--- session 38: the refill climb with a second consumer, through a real branch point (FINDINGS 70) ---"
|
||||||
|
# ROADMAP P6, the oldest item in it: 65.6 and 67.6 both recorded that the slack
|
||||||
|
# table existed and that 51.3's climb had never met a branch point with audio on
|
||||||
|
# the wire. This is that run. It needs the scene graph, so it skips with the
|
||||||
|
# session-24 stage when there is no checkout.
|
||||||
|
#
|
||||||
|
# WHAT IS GATED IS STRUCTURAL, and deliberately not the milliseconds: the
|
||||||
|
# silences move with the scene table and with the one-container-per-scene
|
||||||
|
# assumption the tool prints in its own section 5. What must not move is the
|
||||||
|
# ORDER and the SIGNS -- audio never shortens a climb, the shipped cadence
|
||||||
|
# strands most within-container branch points off a group boundary, F=1 strands
|
||||||
|
# none, and the lump read that removes the silence is an order of magnitude
|
||||||
|
# cheaper than the silence. A tree where any of those flipped has a different
|
||||||
|
# answer to the cadence pick.
|
||||||
|
if [ -f "$DIRKSIMPLE/data/games/lair/game.lua" ]; then
|
||||||
|
python3 tools/analysis/36_branch_audio.py --gate \
|
||||||
|
> tmp/branch_audio.log 2>&1 || { cat tmp/branch_audio.log; exit 1; }
|
||||||
|
grep -aE "^ (OK|FAIL) |SECOND CONSUMER IS|BRANCH-AUDIO GATE|^ mean |^ free " \
|
||||||
|
tmp/branch_audio.log
|
||||||
|
else
|
||||||
|
echo " SKIPPED: no DirkSimple checkout at $DIRKSIMPLE"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "ALL GREEN"
|
echo "ALL GREEN"
|
||||||
|
|||||||
Reference in New Issue
Block a user