Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,66 @@ Checked against `12d4a4f9d`.
- **Edit order with #1621** (`runtime.rs`, the per-node window): whichever is scheduled first lands
first; the other rebases. They change different lines.
- **Dependencies.** #1281, #1280c (for D1's retarget).

## Decision record: #1282 (batch 11)

**Shipped.** A live-controlled per-node effect instance that survives a swap with its prepared
layout unchanged moves by pointer: D1-D3 as frozen.

- `graph`: `LiveControlEffect::can_move_from` / `move_from` (`runtime.rs`, in a realtime-policy
region). The check compares the prepared layout, the control kind and cell shape (the successor
holds its carried slots, #1633) and the shunt shape. The move takes the control state through
`EffectControlLane::carry_from` (unread cells, bypass crossfade, symmetry terms, #1280a), then
exchanges the two processor boxes and the two `BypassShunt`s with `core::mem::swap`. The `spans`
window and the `observation` lane stay (#1621 sizes the window; the tap state is #1327b's), and
the pending `Observe` records of the predecessor are applied at the carry by the second marked
loop, as for every other move. `Runtime::can_carry_effect_lanes` and `carry_effect_lanes` gain the
node-to-node arm. `install_effect_carry` no longer refuses a node-to-node move and
`GraphCarryInstallError::EffectMoveBetweenNodes` is deleted.
- `host-core`: `resolve_effect_carry` drops the node-to-node exclusion, and a node-to-node move
adds nothing to the payload scratch (`carry.scratch_bytes` counts only moves that go through it),
so the carry's retained charge for a session of delays is the move table and the carried slots.
D3 needs no code: a per-node effect without live controls has no inventory row, so its owner
restarts its strip.
- `effect-contract` (outside the authorized list, one mechanical helper): `BypassShunt::same_shape`.
- `graph` test-support: `test_only_carry_payload_bytes` / `test_only_reset_carry_payload_bytes`, a
thread-local count of the layout payload each lane move passes through the scratch.
- Test harness: `effect_run_blocks` (a run of more than `BLOCKS` blocks), `Feed::with_blocks`,
`EffectRun::payload_bytes`; `LivePlan::submit` submits at the plan's own sample rate.

**Departures.** None from the frozen decisions. D1's "prepared bypass" needs no new code: the
inventory layout already carries `bypass`, and `effect_instance_records` refuses a bypass change of
an effect that does not lower it.

**Gates and tests** (`crates/host-core/tests/successor_swap.rs`):

| Gate | Test |
|---|---|
| 1 | `a_delay_tail_rings_on_through_the_swap_at_every_width` (64 blocks; restart run loses the echo) |
| 2 | `unread_values_and_a_live_bypass_cross_the_swap_at_every_width` |
| 3 | `a_prepared_bypass_change_restarts_its_strip_at_every_width` (`restarted_strips() == ["eq0"]`) |
| 4 | `a_delay_moves_by_pointer_at_96_khz_at_every_width` (payload bytes 0, swap block clean); `the_payload_counter_counts_a_lane_move` (the counter reads nonzero for a bank move) |
| D3 | `an_owner_without_live_controls_on_one_side_restarts_at_every_width` |
| D1 live | `a_live_difference_on_a_moved_instance_is_retargeted_at_every_width` (delay time, compressor bypass) |

Red under mutation (each run once, reverted): no processor exchange (gates 1, 2, 4 red), no
`carry_from` (gate 2 red).

**Consequence outside the slice.** `capi`'s `a_prepared_bypass_change_rebuilds_and_renders_the_committed_model`
(#1266 gate 3) used a control that rebuilt by content edits, which restarted the per-node multiband
as a side effect. A content edit now moves the multiband, so the control's rebuilds also replace
the multiband by a fresh instance of the same declaration under a new ID (a prepared change of
that owner), which restarts it and its strip as the bypass rebuild does. The oracle is otherwise
unchanged.

**Measured.** Browser boot high-water growth 20,447,232 bytes (before: 20,447,232; the pin is
20,971,520). No resource estimate or pin moved: `cargo test -p host-core --all-features` (428),
`effect-contract`, `rack`, `graph`, `graph-compiler`, `capi`, `engine`, `host-web --features
host-web/test-support` green; clippy `-D warnings` on `host-core` (no features, default,
all features) and `host-core graph effect-contract rack capi --all-targets --all-features`;
`realtime-policy`, `check-bench-policy.sh`, `check-builtins-fixtures.sh`, `check-cross-targets.sh`,
`check-web-audioworklet.sh`, `trace-graph-audit.sh` (1,000,000 blocks) and rustdoc `-D warnings` pass.

### `audit capi` digest (batch 11)

The C ABI render audit commits one structural transaction after its first call (it adds a muted track). Its session also holds a per-node live effect instance (the audit editor reaches a non-EQ effect lane). Before this slice that instance restarted at the swap; now it moves by pointer and keeps its state. So `audit capi`'s `pcm_digest` moves from `9bc5ec3d8a4c223c` to `bf7dcedd1d92621f`, measured by building the audit at `405221f8d` (#1633, the parent: `9bc5ec3d8a4c223c`) and at `067d145ec` (this slice: `bf7dcedd1d92621f`); the later batch 11 slices leave it at `bf7dcedd1d92621f`. Allocations, frees and render errors stay 0.
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,75 @@ Checked against `12d4a4f9d`.
- **Dependency on #1282.** The lines hold no effect state; the dependency orders the shared files
(`graph/src/{lib.rs,runtime.rs}`, `host-core/src/prepare.rs`, `successor_swap.rs`) only. This
slice can land before #1327 or after it.

## Decision record: #1283 (batch 11)

**Shipped.** D1-D5 as frozen: a compensation line carries when its edge ID exists in both plans
with the same endpoints; equal lengths swap the rings and the cursor, other lengths take a
head-aligned copy.

- `graph/program.rs`: `DelaySpec` gains `edge` (the delayed edge's position in `GraphSpec::edges`),
so the lowered program says which edge each line delays.
- `graph/runtime.rs` (policy regions): `CompensationDelay::adopt` (D2 swap, else D4 copy through
`carry_head_aligned`, one routine for both channels and both directions), and
`Runtime::can_carry_delay_lines` / `carry_delay_lines`, bounded by the move table.
- `graph/lib.rs`: `GraphDelayLine` (edge, source, destination, line, length), `GraphDelayMove`,
`take_compensation_lines`, `install_delay_carry`, `delay_carry_retained_bytes`, three install
errors, `GraphExecutor::carry_delays`, and the check and the carry in `adopt_predecessor` (the
check joins the "every section checks before any section moves" list). `carry_program_retained_bytes`
counts the move table.
- `host-core/prepare.rs`: `PlanStateInventory::delay_lines` (counted in `retained_bytes`),
`resolve_delay_carry` (the join), the table charged against the graph cap, and a program that
names the predecessor and moves no source when a line is the only thing that carries.

**Decisions taken where the text left room.**

- A route's two edges (`RouteSource`, `RouteDestination`) report the route's own endpoints (its
source strip and its destination node), not the edge's. The destination edge's own endpoints do
not change when only the route's source is re-pointed, so the text's "same source and destination
nodes" would have carried the old line there.
- The identity of the lines (`GraphDelayLine`, with ID strings) is not retained in the plan: the
executor holds it until `take_compensation_lines` is called, once, right after bind, and the
inventory (control side, counted) keeps it. The plan retains only the move table.
- The lookup is by successor edge to the predecessor edge of the same ID; #1363 D4 adds the alias.

**Gates and tests.**

| Gate | Test |
|---|---|
| 1 | `compensated_output_lines_keep_their_audio_at_every_rate_and_width` (44.1, 48, 88.2, 96 kHz; each asserts two compensated lines in the predecessor) |
| 2 | `compensated_submix_lines_keep_their_audio_at_every_rate_and_width` |
| 3 | `runtime::tests::a_successor_line_continues_the_predecessors_pending_samples_head_aligned` (equal, longer, shorter, one-word lines; a cursor at every position and wrapped) |
| 4 | `a_repointed_route_does_not_carry_its_line_at_every_rate_and_width` (`restarted_strips()` empty; muted re-pointed strip equals B, audible one differs); `prepare::tests::delay_lines_join_by_edge_and_endpoints`; `tests::a_route_edge_names_the_endpoints_of_its_route` |
| 5 | `assert_clean_swap` in every case above: allocator and render audit zero over the swap block |

Red under mutation (each run once, reverted): no `install_delay_carry` (gates 1, 2, 4 red), join
without endpoints (gate 4 red), copy from index 0 instead of the cursor (gate 3 red).

**Consequence outside the slice.** Two `capi` live tests compared a rebuild with another history
and relied on the lines restarting at rest: `a_live_mute_survives_a_later_rebuild` (live mute one
block before the swap against a mute in the swap transaction) and
`a_prepared_bypass_change_rebuilds_and_renders_the_committed_model` (carried source ring against a
fresh one in the control). With carried lines the audio a line holds at the swap plays out after
it, and in both pairs the held audio differs by one block. They now compare from the block after
the lines flush (the session's latency, in blocks); the oracle is otherwise unchanged. Found, not
from this slice: `host-web` `meter_delivery_restarts_on_the_successor_and_never_publishes_a_misshaped_frame`
fails at the base (lane W's file).

**Measured.** Browser boot high-water growth 20,447,232 bytes (before: 20,447,232; the pin is
20,971,520). No pin moved. `host-core` (442 with all features), `graph` (105), `graph-compiler`
(178), `rack` (67), `capi` (153), `control-plane` (5) green; clippy `-D warnings` on `host-core` (no
features, default, all features) and `host-core graph capi` with all targets; `realtime-policy`,
`check-bench-policy.sh`, `check-workspace-policy.sh`, `check-builtins-fixtures.sh`,
`check-graph-determinism.sh`, `check-cross-targets.sh`, `check-web-audioworklet.sh`,
`trace-graph-audit.sh` (1,000,000 blocks) and rustdoc `-D warnings` pass.

## Fold (batch 11 review)

- The `#1283 D1` join doc sat on the test helper `strip_delay`; it now sits on
`delay_lines_join_by_edge_and_endpoints`, the test it describes.
- `a_live_mute_survives_a_later_rebuild` and `a_prepared_bypass_change_rebuilds_and_renders_the_committed_model`
compared two of six blocks after the lines flushed. Both rigs, the control and the rebuild run now render
`flushed + 6` blocks after the seek and compare `[flushed..]`, so six blocks are compared. The gate-3 comment
now says what the pre-flush window holds: the source ring's audio and audio rendered under the predecessor's
bypass in the carried lines, which no control built from the committed model reproduces.
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,67 @@ Checked against `12d4a4f9d`.
(#1277 D8), then copy the ramp, its position and its mute. No engine edit.
- **Authorized paths** gain `crates/host-core/src/live_delta.rs` (D3's extraction only).
- **Dependencies.** #1283 (shared files). #1225 is removed.

## Decision record: #1284 (batch 8)

**Shipped.** D1, D2 and the amended D3 (carry, then retarget; #1225 D8), as frozen.

- `graph`: `TrackDelayLine::lanes_match` / `adopt` (equal rings and cursors swap, lane by lane),
`LiveRoute::adopt` (ramp, position, mute), `Runtime::{can_carry,carry}_{strip_delays,live_routes}`,
all in policy regions and bounded by the move table. The route carry first applies the
predecessor's pending cell value under `LiveSnapshot::ALL`, then copies. New API:
`GraphStripDelayLine` / `GraphStripDelayMove` (lane mask `STRIP_DELAY_LEFT|RIGHT`),
`GraphLiveRoute` / `GraphRouteMove` (by runtime unit), `take_strip_delay_lines`, `take_live_routes`
(read once after bind, as `take_compensation_lines`), `install_strip_delay_carry`,
`install_route_carry`, the two `*_carry_retained_bytes`, six install errors; both moves join the
"every section checks before any section moves" list. `build_sequential` now also returns the bind
identities.
- `host-core`: `PlanStateInventory::{strip_delays, live_routes}`; `join_strip_delays` and
`resolve_strip_delay_carry` (the key is strip and lane; a lane that carries has an equal
`delay_samples` and a line in both plans; a strip with a changed lane, or whose line one plan lacks,
goes into `restarted`); `join_routes` and `resolve_route_carry` (a route into a submix with the same
ID, source (strip and tap) and destination, and a live route in both plans). D3: the per-route
function is `route_record_between` (+ `RouteEditRamps`, moved from `live_delta.rs`); the classifier
and the join both call it, on effective values (`EffectiveDiff::route_gates`, `gate_length`,
`source_strips`, un-gated from `control-provider`). The retarget is written with
`stamp_held_gate(base.revision)` in the strips' block. `join_strips` takes the `EffectiveDiff`, built
once per preparation. The late carry program (names the predecessor, moves no source) now also
covers a plan whose only carry is a strip delay or a send.

**Departures.**

- `LiveRoute::drain` is renamed `apply_pending`. The call rule of the realtime policy refused
`from.drain(..)` in the move loop, because other functions named `drain` pop a queue. `apply_pending`
holds no pop (it reads a latest-target cell); its one other caller is the block drain in `execute_op`.
- A route carries only if its source and destination are equal. The text says "source, tap and
destination"; the tap is part of `RouteSource`.
- Test harness: `EffectValue::SendGain` and `CellRecord::Route` write a send record. The two
`a_followed_mute_change_*` oracles (the "written to A" run) now also write the send's gate record, as
a live commit does (#1558 D1): with sends carried, a strip mute written to A without the following
send's record is not the state the committed model describes.

**Gates and tests** (`crates/host-core/tests/successor_swap.rs` unless noted):

| Gate | Test |
|---|---|
| 1 | `strip_delays_and_a_send_ramp_cross_the_swap_at_{eight,four}_lanes` (track lanes 37/53, a lane with no delay, submix 41/29, a ramp from block 4 and a record pending at the swap) |
| 2 | `a_changed_lane_delay_restarts_only_its_lane_at_*` (restart set `["eq0"]`, left lane carried, right plane silent), `a_repointed_send_starts_at_its_prepared_value_at_*` (no restart, equals B without records), `only_the_delay_changed_strip_is_restarted_at_*` |
| 3 | `assert_clean_swap` in every case: zero allocations and frees on the swap block |
| 5 | `a_carried_send_is_retargeted_to_the_successors_gain_at_*` (run 1 retarget equals run 2 live write; not vacuous against a stale level) |
| unit | `graph::runtime::tests::a_strip_delay_line_hands_over_the_lanes_of_its_mask`, `prepare::tests::strip_delay_lanes_join_by_strip_and_lane`, `prepare::tests::live_routes_join_by_route_id` |

Red under mutation (each run once, reverted): no strip delay move (gates 1, 2 red), no route move
(gates 1, 2b red), no predecessor drain (gates 1, 5 red), no retarget write (gate 5 red), route join
without the destination (gate 2b red), lanes keyed together (gate 2a red).

**Measured.** Browser boot high-water growth 20,447,232 bytes (before: 20,447,232). No pin moved.
`realtime-policy`, `check-bench-policy.sh`, `check-workspace-policy.sh`, `check-builtins-fixtures.sh`,
`check-graph-determinism.sh`, `check-cross-targets.sh`, `check-web-audioworklet.sh`, rustdoc
`-D warnings` and clippy `-D warnings` (host-core no features, default, all features; graph) pass.
`trace-graph-audit.sh` was not run.

## Fold (batch 11 review)

- `LiveRoute::apply_pending` doc: "The drain runs in every block" is now "The read runs in every block"
(#1347 D3 reads a cell, it drains no queue).
- The two capi oracles that cover the carried lines now compare six blocks after the flush (see #1283).
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Root's ruling on finding F23 of the stored-automation check (`docs/handoffs/stor
`EffectCompileCaps` keeps its shape and `Copy`; its field becomes a ceiling, a capacity above it
or a ceiling of 0 is refused `effect.resource.limit` (`:358`), and host-core passes `u32::MAX`.
- capi: `prepare_caps` (`crates/control-plane/src/compile.rs:601`) drops the field. Its other read,
the protocol's `per_block_automation_density` (`:202`), is deleted by #1629 in batch P1; draft 22
the protocol's `per_block_automation_density` (`:202`), is deleted by #1629 in batch P1; #1668
of the #1058 program then reserves the C field.
- Browser: the derivation from the stored segment count (`hosts/host-web/src/lib.rs:4447-4455`) and
its two uses (`:4463`, `:4494`) go.
Expand All @@ -258,5 +258,5 @@ Root's ruling on finding F23 of the stored-automation check (`docs/handoffs/stor
`crates/capi/tests/resource_lifecycle.rs`, `docs/C_ABI_V1_QUALIFICATION.md` (the formula bullet,
if #1611 and #1621 have not rewritten it), this spec.
- **Dependencies.** #1611 (landed) and #1621 (the per-node window; this issue lands after it). #1629
(batch P1) removes S's protocol read; this issue and draft 22 form batch P2, after P1. The #1058
(batch P1) removes S's protocol read; this issue and #1668 form batch P2, after P1. The #1058
rendering slices no longer depend on this issue.
Loading
Loading