Merge dev into phase-g: Phase Ε/Ρ and the 1.5.0 bump meet Phase Gamma's instrument work; 120/120 green
The per-directory CLAUDE.md count is re-derived at twenty-seven rather than carried from either side. The "Decouple the instrument reload from VST3 activation" TODO entry does not survive: Γ-W3-T1 landed it, and COMPLETED.md carries the discharge.
This commit is contained in:
+171
-87
@@ -110,20 +110,6 @@ Forward-looking follow-ups. Deferred by decision, not oversight — each entry r
|
||||
|
||||
**Done looks like.** Not stated in PLAN.md.
|
||||
|
||||
## FX-GUID keying for `restoreFxOffline` (Design View park/restore)
|
||||
|
||||
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): the bulk of reconcile residuals shipped (`ViewModeModel::reconcile(liveGuids)` prunes orphaned snapshots on every toggle/load; folder restructure is self-healing because the tree is rebuilt each toggle; membership is intentionally kept so undo-delete preserves the tag). Two sub-items were left deferred out of that; this is the first.
|
||||
|
||||
**The wart.** `restoreFxOffline` currently restores per-FX offline state by slot index. If the FX chain is reshuffled while a track is parked, restore lands on whatever plugin now occupies that slot rather than the plugin it was originally captured from.
|
||||
|
||||
**Intended fix.** FX-GUID keying — key the per-FX offline snapshot entries by FX identity rather than slot index.
|
||||
|
||||
**The constraint the fix MUST handle.** The keying change requires a snapshot-schema migration; CONTEXT.md names this alongside the keying change as the reason the fix was deferred rather than folded into the reconcile-residuals work.
|
||||
|
||||
**Priority / risk.** Not stated in the source.
|
||||
|
||||
**Done looks like.** Not stated in the source beyond the fix description above.
|
||||
|
||||
## Dormant membership entries in persisted `view_state`
|
||||
|
||||
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section), the second of the two sub-items left deferred after the reconcile-residuals ship described above.
|
||||
@@ -318,6 +304,15 @@ buffer.
|
||||
shared with every other caller in `core/capture/wav_codec`; a fix must not change
|
||||
those callers' contract or add a second WAV-building code path to maintain.
|
||||
|
||||
**Re-confirmed still accurate (2026-08-02), after the mono-collapse landing touched
|
||||
`wav_codec` adjacent to this site.** `applyMonoCollapse` operates on the staged bytes in
|
||||
`bake_landing.cpp`'s `prepareLanding`, upstream of and unrelated to `runBake`'s
|
||||
`std::vector<double>` copy in `instrument_bake.cpp`; `buildFloat32Wav`'s signature is
|
||||
unchanged. The wart stands exactly as described above.
|
||||
|
||||
**Current blocker.** Not taken this wave because `instrument_bake.cpp` is being edited
|
||||
by a live VST3-parameter track.
|
||||
|
||||
**Priority / risk.** Low / deferred. Logged at Ξ-W2-T1's review; correctness is
|
||||
unaffected, only peak memory on a large bake.
|
||||
|
||||
@@ -507,7 +502,7 @@ needs no live REAPER process to exercise `rec->Register(...)` calls. Once
|
||||
**The constraint the fix MUST handle.** The extraction alone buys nothing:
|
||||
`action_registry` has no test target today either, so lifting `ingestHandleSectionCommand`
|
||||
into it without also standing up the test target just relocates the untested code. The
|
||||
same follow-up could collapse `ingest.cpp:466-472`'s hand-rolled `command_id`+`gaccel`
|
||||
same follow-up could collapse `ingest.cpp`'s `ingestRegisterActions` hand-rolled `command_id`+`gaccel`
|
||||
pair onto `action_registry::registerAction`, which already does exactly that dance for
|
||||
the Q-W6 table.
|
||||
|
||||
@@ -542,7 +537,7 @@ track", nothing to do and the inference is retired into fact. If it comes back "
|
||||
summed file", the refusal is over-strict for the TRACK scope and should be narrowed back
|
||||
— and the ITEM-scope half is then an OPEN question, not settled: a full-extent item
|
||||
capture already sums a multi-track item selection via `&32|single-file`
|
||||
(`tests/test_render_settings.cpp:262`), so if `&128` also sums, a ranged item capture
|
||||
(`test_render_settings.cpp`'s `testMultiTrackStemRenderIsNamedForRefusal`), so if `&128` also sums, a ranged item capture
|
||||
routed through it sums too, and keeping the item refusal in that branch would make item
|
||||
scope inconsistent with itself across the range boundary (full-extent sums, ranged
|
||||
refuses, same scope). Whether that inconsistency is acceptable or the item refusal should
|
||||
@@ -622,44 +617,6 @@ select/move the neighbour, or capture at track scope instead.
|
||||
non-isolation as an oversight and re-propose closing it against the recipe's stated
|
||||
tracks-and-range-only shape.
|
||||
|
||||
## Resample-bake landings don't apply the lossless mono collapse to a dual-mono render
|
||||
|
||||
**Context (surfaced by Ψ-W2-T2, mono-collapse).** The collapse (`collapseCapturedFileToMono`
|
||||
/ `core/capture/wav_codec::collapseToMono`) ships for every extension capture path —
|
||||
offline, realtime, batch, recapture — but not for `bake_land.cpp`'s landing, the
|
||||
resample bake's `prepareLanding` / `commitLanding` pair. A dead-center instrument render (the common case
|
||||
that motivated Ψ.6 in the first place) is exactly the dual-mono shape the predicate
|
||||
collapses, so an un-collapsed bake keeps paying for the second channel it doesn't need.
|
||||
|
||||
**Not deferred for the reason once given.** `prepareLanding` reads the staged file into
|
||||
`prep.bytes` once, parses its layout, hashes it and derives the channel count from that
|
||||
same one buffer, and `commitLanding` writes that buffer — so collapsing it right after the
|
||||
layout parse would keep the hash, the channel count, and the written file consistent by
|
||||
construction; there is no ordering hazard here to defer around.
|
||||
|
||||
**The real reason.** `bake_land.cpp` is Phase Ξ's freshly-landed surface
|
||||
(Ξ-W2-T1, the resample bake chain) and another team is actively remediating it. Landing
|
||||
a mutation there now would cross tracks mid-remediation for no urgent gain — the mono
|
||||
propagation this item would add is a size win, not a correctness one.
|
||||
|
||||
**A mono capture already propagates through the bake for free**, so this item is scoped
|
||||
to the dual-mono-*render* case only: `runBake` / `instrument_bake.cpp` already renders
|
||||
however many channels the dialed sound has, and `bake_render.cpp:38` reads
|
||||
`sample.channelCount()` off that render rather than hardcoding 2 — a mono-programmed
|
||||
sound already bakes to a mono file today, with no change needed.
|
||||
|
||||
**Intended fix.** Once `bake_land.cpp` is quiet, call `collapseToMono` on `prep.bytes` in
|
||||
`prepareLanding` right after the layout parse and before the hash, matching the
|
||||
offline/realtime insertion point (post-parse, pre-identity-read).
|
||||
|
||||
**Priority / risk.** Low — a size optimization on an already-correct path, not a
|
||||
precision-invariant gap; the bake's dual-mono case still lands as a valid (if larger)
|
||||
stereo file today.
|
||||
|
||||
**Done looks like.** A dead-center instrument bake lands as a 1-channel file with
|
||||
`Sample::channelCount` matching, the same way an offline dead-center capture does; a
|
||||
true-stereo bake is byte-identical to today's output.
|
||||
|
||||
## A 0-byte render can still pass every gate under Auto/Manual tail (closed)
|
||||
|
||||
**Context (surfaced by Ψ-W3 review).** `OfflineRenderBackend::capture`'s exists-check
|
||||
@@ -682,37 +639,106 @@ refusal reuses `CaptureStatus::BoundsMismatch` rather than minting its own statu
|
||||
earlier note here preferred a distinct status, and that preference is unresolved, not
|
||||
withdrawn.
|
||||
|
||||
## An offline capture can be refused for a short render — root cause open
|
||||
## `renderHonoredBounds`'s one-frame tolerance is empirical, not proven
|
||||
|
||||
**Symptom (live, 2026-08-02).** A capture over [0.000000s, 4.067797s) at 48 kHz was
|
||||
refused: `Render produced 195216 frames but the requested range is 195254`. 38 frames
|
||||
short — 38x the gate's one-frame tolerance, so the tolerance is not what refused it.
|
||||
**Context.** The millisecond-floor defect that motivated this gate is closed
|
||||
(`docs/COMPLETED.md`), but the gate itself — `render_window.h`'s
|
||||
`renderHonoredBounds` — carries a one-frame tolerance that carried through the fix
|
||||
unchanged and was never itself proven.
|
||||
|
||||
**Hypothesis A — the render bounds itself to the media it can see.** REAPER's
|
||||
selected-items render source (`&32`) derives its bounds from the selected items' own
|
||||
extents (`src/core/capture/CLAUDE.md` §Gotchas — itself an inference from an observed
|
||||
defect, not a header fact). If a time-bounded selected-tracks render (`&128`) does the
|
||||
same thing against content extent, a range running past the end of its material comes up
|
||||
exactly as short as the material is.
|
||||
**The wart.** A renderer that resolves the window's two edges by DIFFERENT
|
||||
conventions can sit two frames from `frameCountFor`'s answer on a
|
||||
correctly-honored render. That cannot account for the 8- and 38-frame shortfalls
|
||||
the floor produced (`docs/COMPLETED.md`), so it was not the cause of those
|
||||
refusals — but it means a future one- or two-frame refusal may be the gate's own
|
||||
edge convention rather than a real defect.
|
||||
|
||||
**Hypothesis B — a trailing-silence trim fires anyway.** `TailMode::None` sets
|
||||
`RENDER_NORMALIZE = &(4<<16)` (disable all postprocessing) and `RENDER_TRIMEND = 0`. If
|
||||
REAPER trims regardless of that bit, a range whose material decays before its end loses
|
||||
exactly the decayed frames.
|
||||
**Intended fix.** Not proposed. Widening the tolerance is a precision-invariant
|
||||
decision, not a bug fix, and was deliberately not taken on speculation.
|
||||
|
||||
**Not excluded — the gate itself.** `renderHonoredBounds`' one-frame tolerance is
|
||||
empirical, not proven (`src/core/capture/render_window.h`): a renderer that resolves the
|
||||
window's two edges by DIFFERENT conventions can sit two frames from `frameCountFor`'s
|
||||
answer on a correctly-honored render. That cannot account for 38 frames, so it is not
|
||||
this refusal — but it means a future one- or two-frame refusal may be ours, which is why
|
||||
the tolerance was not widened on speculation. Widening it is a precision-invariant
|
||||
decision, not a bug fix.
|
||||
**Priority / risk.** Low. Nothing to date implicates the tolerance itself;
|
||||
recorded so a future narrow refusal is investigated rather than assumed to be
|
||||
the same floor.
|
||||
|
||||
**How it gets decided.** `docs/VERIFICATION.md` §Capture range and bounds, the three
|
||||
numbered blocker steps: step 1 separates A's `&32` path from the shared `&128` path (and
|
||||
says how to tell when it failed to), step 2 asks whether the render is short at all, step
|
||||
3 reads the retained refused render to place the missing frames. Nothing here should be
|
||||
"fixed" before that comes back.
|
||||
**Done looks like.** Either the tolerance is confirmed correct by a DAW
|
||||
observation that isolates edge-convention behavior from bounds-floor behavior,
|
||||
or it is widened with the reasoning recorded.
|
||||
|
||||
## `TailMode::Auto` and `Manual` have no automatic bounds observation
|
||||
|
||||
**Context.** `render_bounds_gate.cpp`'s `checkRenderedBounds` returns early for
|
||||
anything but `TailMode::None`, so the millisecond-floor fix (`docs/COMPLETED.md`)
|
||||
was measured only against `TailMode::None` — Auto and Manual were never
|
||||
observed, before the fix or after it.
|
||||
|
||||
**The wart.** The inference that Auto/Manual are fixed too is sound — same
|
||||
bounds path, same floor, same fix — but it is an inference, not a measurement.
|
||||
`checkRenderedFileNotEmpty` runs on every tail mode and still catches a 0-byte
|
||||
render, but that is the ONLY automatic bounds signal Auto/Manual get; a
|
||||
floored or otherwise short-but-nonzero render under either mode would land as
|
||||
`Ok` with nothing to catch it.
|
||||
|
||||
**Intended fix.** Not a code change — a DAW observation. `docs/VERIFICATION.md`'s
|
||||
"Capture range and bounds" section already carries the manual check: repeat an
|
||||
off-grid-start capture at Manual over a source loud to the window's end and
|
||||
check the landed frame count against window + `tailMs`; Auto can't be checked
|
||||
by count (it trims trailing silence) and needs the null test by ear/inversion
|
||||
instead.
|
||||
|
||||
**Priority / risk.** Low. Both modes share the same bounds path as the
|
||||
now-fixed `TailMode::None`, so nothing suggests they still floor — but nothing
|
||||
confirms it either.
|
||||
|
||||
**Done looks like.** A DAW-observed Auto and Manual capture, each landing the
|
||||
window as requested, closes the inference into fact — or surfaces a
|
||||
mode-specific divergence this entry does not currently know about.
|
||||
|
||||
## Floor, ceil and round are not the identity on a millisecond grid point in binary double (caution, not an open question)
|
||||
|
||||
A discarded compensation design for the millisecond-floor defect
|
||||
(`docs/COMPLETED.md`) rested on the premise that a grid-aligned value survives a
|
||||
bare floor/ceil/round unchanged. That is false in binary double: `1.007 * 1000
|
||||
== 1006.9999999999999` (floors to 1006, not 1007), and `4.068 * 1000 ==
|
||||
4067.9999999999995` (floors to 4067, not 4068). The compensation this premise
|
||||
would have supported is no longer needed — the fix moved the render to a bounds
|
||||
mode that does not floor at all — so this is not a live open question. Recorded
|
||||
because it would bite any future millisecond-grid arithmetic that assumes an
|
||||
on-grid value is safe from a bare floor: `render_window.h`'s own
|
||||
`isOnMillisecondGrid`/`msFlooredEndFrameCount` already carry the nanosecond
|
||||
tolerance that handles it correctly on this codebase's side of the boundary; the
|
||||
trap is for whoever writes the next piece of grid arithmetic without that guard.
|
||||
|
||||
## `capture.cpp` is over the ~600-line ceiling — the seam is identified, taking it is blocked
|
||||
|
||||
**Context.** Removing the settled bounds experiment's instrumentation (the console
|
||||
verdict and the three-checkpoint `RENDER_STARTPOS`/`ENDPOS` read-back) brought the file
|
||||
from 697 to **620 measured lines**, against root `CLAUDE.md`'s ~600-line ceiling. The
|
||||
seam that entry originally named is gone with the instrumentation; nothing left in the
|
||||
file is bisectable without cutting load-bearing why.
|
||||
|
||||
**The remaining seam is a real responsibility boundary**, and the file header already
|
||||
names it as two things: `OfflineRenderBackend::capture` (the offline render driver)
|
||||
versus the four helpers BOTH backends share — `makeUniqueTag`, `captureNameFor`,
|
||||
`collapseCapturedFileToMono`, `stampCaptureSample` — consumed by `capture_batch`,
|
||||
`capture_orchestrator`, `capture_realtime_shell`, `capture_realtime_finalize` and
|
||||
`render_in_place`. Lifting those four into their own TU takes the driver under the
|
||||
ceiling and gives the cross-backend steps their own home.
|
||||
|
||||
**Why not taken.** `src/shell/capture/` has no `CMakeLists.txt` of its own — its sources
|
||||
are listed in `src/app/CMakeLists.txt`, so a new TU needs an edit there. Forcing the
|
||||
four helpers into an existing TU instead (orchestrator, realtime finalize) would put
|
||||
them in a wrong home to dodge one build-file line, which is worse than the overshoot.
|
||||
|
||||
## bext TimeReference read-back is not a floor detector (dead end, recorded so it is not re-litigated)
|
||||
|
||||
Idea considered and dropped: read a captured file's `BWF:TimeReference` tag back as
|
||||
independent evidence on the START-edge millisecond-floor question above. `WDL/metadata.h`'s
|
||||
`WriteMetadataPrefPos` only writes it past its `prefpos > 0.0` guard (`:1301`) — that guard
|
||||
alone is enough to rule the approach out. One nuance worth recording separately: the
|
||||
millisecond quantization at `:1382-1383` (`AddMexMetadata`'s `ParseUInt64(val)/1000.0`)
|
||||
belongs to the MEX caller, not proven to be `WriteMetadataPrefPos`'s own behavior or the
|
||||
renderer's direct call into it — so even without the guard, a floored bext tag would show
|
||||
that MEX quantizes, not that the render engine does.
|
||||
|
||||
## Split `render_bounds_gate` on the verdict/message vs. filesystem seam
|
||||
|
||||
@@ -744,8 +770,8 @@ the two callers' plumbing.
|
||||
|
||||
**Context.** `saveToActiveProject()` returns false for exactly two reasons — no active
|
||||
project, or an unsaved one — and in both cases NOTHING was written. Four capture sites
|
||||
discard that return outright: `capture_orchestrator.cpp:343`, `capture_batch.cpp:266` and
|
||||
`:333`, and `realtime_lifecycle.cpp:39`.
|
||||
discard that return outright: `capture_orchestrator.cpp`'s `RunCapture`, `capture_batch.cpp`'s
|
||||
`RunBatchCaptureItems` and `RunBatchCaptureRazor`, and `realtime_lifecycle.cpp`'s `CommitRealtimeResult`.
|
||||
|
||||
**The wart.** A capture on an unsaved project renders the file into the bank folder, adds
|
||||
the `Sample` to the in-memory book, records a birth record in memory — and loses all three
|
||||
@@ -766,7 +792,7 @@ and the choice between those two is recorded rather than implicit.
|
||||
|
||||
## `panel_input`'s wheel handler persists the whole book per wheel message
|
||||
|
||||
**Context.** `panel_input.cpp:450` — `handleWheel` calls `markTailDirty()` on every wheel
|
||||
**Context.** `panel_input.cpp` — `handleWheel` calls `markTailDirty()` on every wheel
|
||||
message that actually moves `manualMs`, while the pointer is over the footer in Manual
|
||||
mode. (It coalesces sub-notch deltas within ONE message and no-ops at a bound, so the
|
||||
count is wheel messages that changed the value, not raw notches.)
|
||||
@@ -785,10 +811,10 @@ the value that lands is the gesture's final one.
|
||||
|
||||
## `RunCaptureItemAssign`'s undo point does not follow the pattern its comment claims
|
||||
|
||||
**Context.** `capture_orchestrator.cpp:364-365` states that the action follows the bank-op
|
||||
family's discard-on-unsaved pattern.
|
||||
**Context.** `capture_orchestrator.cpp`'s `RunCaptureItemAssign` states that the action follows
|
||||
the bank-op family's discard-on-unsaved pattern.
|
||||
|
||||
**The wart.** It does not: `:382-383` records the undo point unconditionally whenever
|
||||
**The wart.** It does not: `RunCaptureItemAssign` records the undo point unconditionally whenever
|
||||
`sampleId` is non-empty, and never consults the persist's return at all. So on an unsaved
|
||||
project it records an undo point for ext-state that was never written — the empty
|
||||
no-effect entry `persistBankOp`'s guardrail exists to avoid. The comment describes the
|
||||
@@ -803,7 +829,8 @@ the unsaved-project case one way.
|
||||
|
||||
## `core/tracking/CLAUDE.md`'s untracked-file enumeration says "reaches the `.rpp`" too loosely
|
||||
|
||||
**Context.** `src/core/tracking/CLAUDE.md:24-31` enumerates how a created file can stay
|
||||
**Context.** `src/core/tracking/CLAUDE.md` §"Invariants" — "No silent gaps — in memory at
|
||||
creation, on disk at the next save" — enumerates how a created file can stay
|
||||
untracked, and describes the ledger as reaching the `.rpp` at the following
|
||||
`saveToActiveProject()`.
|
||||
|
||||
@@ -837,3 +864,60 @@ in the `.rpp`", and does not gain a second home for the distinction.
|
||||
**Priority / risk.** Low. Nothing here is load-bearing on the frozen contract: the id table, the plain ranges and the norm↔plain laws are all decided and tested without a host.
|
||||
|
||||
**Done looks like.** Each of the four exercised once in REAPER, with the unit-rendering answer recorded and, if it went the other way, the one-line formatter change made.
|
||||
|
||||
## `view_mode_model.cpp` is over the ~600-line structural bar, and `view.cpp` is close behind
|
||||
|
||||
**Context (surfaced by the FX-GUID keying track).** Root `CLAUDE.md`'s structural
|
||||
heuristics put an ~600-line ceiling on any one file, with a documented responsibility
|
||||
seam as the required method for splitting it, not an arbitrary bisection.
|
||||
`src/core/view/view_mode_model.cpp` measures **815 lines** (verified this pass),
|
||||
up from 715 before the FX-GUID keying track's v2 schema addition made it worse.
|
||||
|
||||
**The named seam.** The JSON codec — `serialize()`/`deserialize()` — wants its own
|
||||
`view_state_codec` TU in `src/core/view/`.
|
||||
|
||||
**Why it was deferred, and this reasoning should survive.** `serialize()` is a
|
||||
`ViewModeModel` member and `deserialize()` a static factory (confirmed:
|
||||
`std::string ViewModeModel::serialize() const` and
|
||||
`std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string&)`), both
|
||||
reaching private state — so extraction needs either a friend declaration or a new
|
||||
public accessor surface. Doing that in the same commit that changed the byte format
|
||||
the golden test literals pin would roll a format change and a codec extraction
|
||||
together, which is the riskier order.
|
||||
|
||||
**Also over the bar, blocked differently.** `src/shell/view/view.cpp` measures
|
||||
**642 lines** (verified this pass). Its seam is blocked not by a private-state/friend
|
||||
question but by a build file another team owns: `src/shell/view/` has no
|
||||
`CMakeLists.txt` of its own today.
|
||||
|
||||
**Priority / risk.** Not stated.
|
||||
|
||||
**Done looks like.** `view_mode_model.cpp`'s JSON codec is extracted into its own
|
||||
`view_state_codec` TU (with the friend/accessor question resolved deliberately, not
|
||||
sidestepped), dropping the file under the ~600-line ceiling; `view.cpp`'s own path is
|
||||
unblocked once the build-file ownership question is resolved.
|
||||
|
||||
## FX-GUID stability for `restoreFxOffline` is unverified in the DAW
|
||||
|
||||
**Context.** The Design View park/restore FX keying (`restoreFxOffline`,
|
||||
`src/shell/view/view.cpp`) rests on `TrackFX_GetFXGUID` returning an identity that
|
||||
survives a chain reorder while a track is parked. SWS issue #802 reports that after
|
||||
`SNM_MoveOrRemoveTrackFX` reorders a chain, the FXID lines do not follow the plugin
|
||||
(`SNM_PreObjectState()` → `RemoveAllIds()`) — if that still holds, an SWS-driven
|
||||
reorder while parked produces wrong-plugin restores or mass drops, which is the exact
|
||||
operation this keying targets.
|
||||
|
||||
**What must be checked.** Native drag-reorder, an SWS move, save/reload, and two live
|
||||
instances of the same plugin.
|
||||
|
||||
**Already flagged in code — this entry is the tracked home, not a restatement.**
|
||||
There is a `[verify — DAW]` marker at `fxGuidString` in `src/shell/view/view.cpp` and
|
||||
a note in `src/shell/view/CLAUDE.md`'s Gotchas; point at them rather than restating
|
||||
them in full.
|
||||
|
||||
**Priority / risk.** Not stated.
|
||||
|
||||
**Done looks like.** Native reorder, SWS reorder, save/reload, and a
|
||||
two-instance-of-the-same-plugin case are each observed in a live REAPER session, and
|
||||
either the identity is confirmed to survive all four, or a degradation is found and
|
||||
the keying is amended.
|
||||
|
||||
Reference in New Issue
Block a user