capture: close batch-quarantine silence, 0-byte asymmetry, and round-two doc overclaims

Batch captures now name the retained-render folder once instead of nothing; Auto/Manual tail modes refuse a 0-byte render like None does; VERIFICATION.md steps 1-3 no longer invite a false conclusion; docs/comments no longer overclaim.
This commit is contained in:
2026-08-02 08:00:12 -04:00
parent 2005f90c66
commit 0511d16d4f
10 changed files with 127 additions and 26 deletions
+40 -11
View File
@@ -689,7 +689,7 @@ 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 still pass every gate under Auto/Manual tail (narrowed, not closed)
## A 0-byte render can still pass every gate under Auto/Manual tail (closed)
**Context (surfaced by Ψ-W3 review).** `OfflineRenderBackend::capture`'s exists-check
passes for a 0-byte file, and the bounds gate used to fire only when `expectedFrames > 0`
@@ -697,22 +697,25 @@ passes for a 0-byte file, and the bounds gate used to fire only when `expectedFr
refusing, so a 0-byte render reached `stampCaptureSample` and landed as
`CaptureStatus::Ok` with an empty `contentHash` and `channelCount == 0`.
**Narrowed.** `shell/capture/render_bounds_gate` now refuses an unmeasurable render
(invalid layout, or a layout declaring no sample rate) instead of skipping it. That
covers `TailMode::None` only — the gate does not judge Auto/Manual, which add frames by
design, so a 0-byte render under either of those still lands as `Ok`. 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.
**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.** Reject a 0-byte / unparseable render right after the exists-check, on
every tail mode, 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 — two orders of magnitude outside the gate's one-frame tolerance, so the tolerance
is not what refused it.
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
@@ -739,3 +742,29 @@ numbered blocker steps: step 1 separates A's `&32` path from the shared `&128` p
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.