Merge capture-bounds diagnosis: empirical tolerance, unmeasurable-render refusal, refused renders kept for inspection
This commit is contained in:
+2
-1
@@ -831,7 +831,8 @@ code, which makes the byte-identity regression floor structural rather than hope
|
||||
and makes the fix cheap to revert if the underlying inference proves wrong. Also added:
|
||||
a transient isolation guard cutting `B_MAINSEND` on direct folder children and muting
|
||||
receives so an item capture stays true to item scope, and a post-render frame-count
|
||||
gate (±1 tolerance, tail-None only) that refuses and self-cleans a widened render. New
|
||||
gate (±1 tolerance, tail-None only) that refuses a widened render and retains it outside
|
||||
the bank for diagnosis rather than deleting it. New
|
||||
modules `core/capture/render_window`, `core/capture/track_topology`,
|
||||
`shell/capture/render_selection`, `shell/capture/render_isolation`.
|
||||
|
||||
|
||||
+2
-2
@@ -2199,8 +2199,8 @@ full-extent case runs literally unchanged code, keeping the byte-identity regres
|
||||
floor structural and the fix cheap to revert if the override inference proves wrong.
|
||||
Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder
|
||||
children, muting receives) so an item capture stays true to item scope, and a
|
||||
post-render frame-count gate (±1 tolerance, tail-None only) that refuses and self-cleans
|
||||
a widened render. New modules `core/capture/render_window`, `core/capture/track_topology`,
|
||||
post-render frame-count gate (±1 tolerance, tail-None only) that refuses a widened
|
||||
render and retains it outside the bank for diagnosis rather than deleting it. New modules `core/capture/render_window`, `core/capture/track_topology`,
|
||||
`shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests
|
||||
on the unverified inference that REAPER's selected-tracks render source overrides
|
||||
custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification
|
||||
|
||||
+75
-12
@@ -689,19 +689,82 @@ stereo file today.
|
||||
`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 pass every gate and land as `Ok` (pre-existing, not a Ψ-W3 regression)
|
||||
## A 0-byte render can still pass every gate under Auto/Manual tail (closed)
|
||||
|
||||
**Context (surfaced by Ψ-W3 review).** `OfflineRenderBackend::capture`'s exists-check
|
||||
(`capture.cpp:489`) passes for a 0-byte file, and the bounds gate (`:507-546`) only fires
|
||||
when `expectedFrames > 0` — an invalid/empty layout reads `expectedFrames == 0` and skips
|
||||
the gate rather than refusing. A 0-byte render can therefore reach `stampCaptureSample`
|
||||
and land as `CaptureStatus::Ok` with an empty `contentHash` and `channelCount == 0`.
|
||||
passes for a 0-byte file, and the bounds gate used to fire only when `expectedFrames > 0`
|
||||
— an invalid/empty layout read `expectedFrames == 0` and skipped the gate rather than
|
||||
refusing, so a 0-byte render reached `stampCaptureSample` and landed as
|
||||
`CaptureStatus::Ok` with an empty `contentHash` and `channelCount == 0`.
|
||||
|
||||
**Not introduced by Ψ-W3.** The exists-check and the `expectedFrames > 0` guard both
|
||||
predate this track; Ψ-W3 only added the mono-collapse failure report that sits downstream
|
||||
of this hole and was careful not to assert bytes it never verified (see
|
||||
`reportCollapseFailure` in `capture.cpp`).
|
||||
**Narrowed, then reopened as an asymmetry.** `shell/capture/render_bounds_gate` was
|
||||
first changed to refuse an unmeasurable render (invalid layout, or a layout declaring no
|
||||
sample rate) instead of skipping it — but that gate only ever judges `TailMode::None`,
|
||||
so a 0-byte render under Auto/Manual still landed as `Ok`, while `None` now refused and
|
||||
quarantined the identical file. The two tail modes disagreed on a defect neither should
|
||||
accept.
|
||||
|
||||
**Intended fix.** After the exists-check, also reject a 0-byte file explicitly (its own
|
||||
status, not folded into `BoundsMismatch`, since a 0-byte file was never bounds-checked at
|
||||
all) before anything downstream reads it.
|
||||
**Closed.** `capture.cpp` now checks `checkRenderedFileNotEmpty` right after the
|
||||
exists-check, on every tail mode, before the `TailMode::None`-only bounds gate runs —
|
||||
a 0-byte render is refused and quarantined identically regardless of tail mode. The
|
||||
refusal reuses `CaptureStatus::BoundsMismatch` rather than minting its own status; the
|
||||
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
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## Split `render_bounds_gate` on the verdict/message vs. filesystem seam
|
||||
|
||||
**Context (Ψ-W3 round-two review).** `render_bounds_gate.cpp` mixes pure verdict
|
||||
composition (frame-count comparison, message text) with filesystem I/O
|
||||
(`retainRefusedRender`'s `fs::create_directories`/`fs::rename`) in one shell TU. The
|
||||
verdict half has no REAPER dependency and no filesystem dependency either — it could be
|
||||
`core/capture`, unit-tested directly instead of only through the pure `render_window`
|
||||
functions it calls. The reviewer's suggested split: verdict + message composition pure
|
||||
and testable in `core/capture`, leaving only `retainRefusedRender` (and the two thin
|
||||
`checkRendered*` entry points that call it) in `shell/capture`.
|
||||
|
||||
**Why deferred.** Out of scope for the dispatch that surfaced it — a structural split,
|
||||
not the bug fix in front of it.
|
||||
|
||||
**Filed also because it's already slightly wrong today.** `render_bounds_gate.cpp`
|
||||
touches no REAPER API (it is `<filesystem>` + the pure `core/capture` modules only), so
|
||||
`src/shell/capture/CLAUDE.md`'s "this directory is the REAPER API surface only" scope
|
||||
line no longer describes it — one more small argument for eventually moving the
|
||||
REAPER-free half to `core/capture`, separate from the untested-filesystem-code gap
|
||||
above.
|
||||
|
||||
**Done looks like.** `core/capture` owns a pure `checkRenderedBoundsVerdict`-shaped
|
||||
function under a `<module>_tests` target with no REAPER, no VST3 SDK, and no
|
||||
filesystem includes; `shell/capture/render_bounds_gate` shrinks to the file-move and
|
||||
the two callers' plumbing.
|
||||
|
||||
@@ -26,6 +26,10 @@ Checks for Θ, Ξ, and Ψ work that no unit test can close. Build **Release**, i
|
||||
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`PLAN.md:2136`)
|
||||
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`PLAN.md:2137`)
|
||||
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:829`)
|
||||
- [ ] **Open blocker — root cause unknown; the three steps below are the experiment** (both live hypotheses and what is NOT yet excluded: `docs/TODO.md` §An offline capture can be refused for a short render). A live capture over [0.000000s, 4.067797s) was refused 38 frames short (195216 of 195254 at 48 kHz). Set View → time unit to Samples first
|
||||
- [ ] **Step 1 — does the shortfall follow the render source?** This only tests anything if item scope actually reaches REAPER's selected-items render, and it does that ONLY when the selected items' extent already equals the requested window (`itemExtentPrintsWindow`, `src/core/capture/render_window.h`); otherwise item scope re-sources through the items' own tracks — the same source track scope uses, so the two runs would test one thing twice. So: snap the time selection to the item's exact start and end, run **item** scope, then **track** scope over the identical range. Report both `Render source:` lines and both frame counts. **If both lines read `selected tracks via master`, the item path was NOT exercised** — the extents did not match; re-snap and repeat before concluding anything. **A landing `&32` run here is not evidence `&32` honours custom bounds** — at a window snapped to the item's own extent, a render that honours the window and one that bounds itself to media content print IDENTICAL frames, so this step cannot tell those two apart; it only tells you which render source is in play. **If neither run refuses at this snapped range, the blocker did not reproduce here** — this range does not recreate the original refusal, which ran past the end of its media; move to step 2, which does
|
||||
- [ ] **Step 2 — full-length or short?** Extend the same range ~1 s past the end of all media, **track** scope. Landing with the full range (no refusal, the card reads the extended length) rules out BOTH a trailing-silence trim and a content-extent bound at once — but it also means there is no refused render for step 3 to read; re-run the ORIGINAL refusing range ([0.000000s, 4.067797s), track scope) to produce one before continuing. A short render does NOT tell the two hypotheses apart: a trim firing despite `RENDER_NORMALIZE &(4<<16)` and a render bounding itself to content extent produce the same count — and that render IS the one step 3 reads. Report which happened, then run step 3
|
||||
- [ ] **Step 3 — where are the missing frames?** Reads the short render from step 2 (or, if step 2 landed, the fresh refused render from re-running the original range per step 2's note) — not anything step 1 may have left behind, since a correctly-snapped step 1 should not have refused at all. A refused render is kept deliberately, not deleted: it is moved to `<project folder>/reasampler_refused/`. **Follow the path in the refusal line, not this sentence** — if the move itself failed the file stays in the bank folder, unindexed, and the line says which happened. Filenames carry a timestamp/counter but no scope marker, so if more than one file has landed in `reasampler_refused/` by now, the one from step 2 is the most recently written one — or empty the folder before running step 2 so there is only one candidate. Insert it against the source over the same range and report whether the head aligns. Frames missing from the TAIL with an aligned head fits either a tail trim or a content-extent bound; a head offset fits neither and is a start-position defect. Also report whether the media under the range ends before the range does. Delete `reasampler_refused/` when done — nothing in the bank references it
|
||||
|
||||
## Names and channels
|
||||
|
||||
|
||||
Reference in New Issue
Block a user