Close three critical review findings on the render-bounds-channel verdict
Verdict can no longer print a false EXACT on an on-grid end, no longer names a bounds channel a content-derived render never consulted, and the grid-align doc premise is corrected without implementing it.
This commit is contained in:
+69
-20
@@ -710,37 +710,86 @@ 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
|
||||
## An offline capture can be refused for a short render — the millisecond floor
|
||||
|
||||
**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.
|
||||
**Measured cause (live, 2026-08-02).** Two captures at 48 kHz, `TailMode::None`, matched
|
||||
their landed frame count to the frame with their window's END floored to the millisecond:
|
||||
`[0s, 1.6551724137931001s)` printed 79440 of 79448 (the console's own `%.17g` read-back),
|
||||
and `[0s, ~4.067797s)` — the double nearest the six-decimal value the original refusal
|
||||
actually printed, `4.0677966101694913`, not itself a captured console value — printed
|
||||
195216 of 195254. A three-checkpoint read-back on `RENDER_STARTPOS`/`RENDER_ENDPOS` was
|
||||
silent at store time and immediately before the render, so REAPER writes the floored end
|
||||
back as a side effect of rendering, not before it. Mechanism, why two
|
||||
`RENDER_BOUNDSFLAG` channels exist, and the live experiment this observation opened:
|
||||
`src/core/capture/render_settings.h`'s `RenderBoundsChannel` — the one narrative home;
|
||||
this entry stays the record of what was actually measured.
|
||||
|
||||
**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.
|
||||
**Disproven by that observation.** The two hypotheses this entry previously carried — that
|
||||
the render bounds itself to the media it can see, and that a trailing-silence trim fires
|
||||
despite `RENDER_NORMALIZE = &(4<<16)` — both predict a shortfall tracking CONTENT. This
|
||||
one tracks the WINDOW: it is the exact ms-floored count, whatever the material does.
|
||||
Neither is the cause. Do not reinstate either without a fresh observation.
|
||||
|
||||
**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.
|
||||
**Still open — the START edge.** Every observation to date started at `0s`. Floor, ceil
|
||||
and round all leave `0s` alone (it is exactly representable in binary), so nothing is
|
||||
known about whether the start floors too, and it is the case that matters most: an end
|
||||
floor refuses loudly, a start floor would shift content and break the null test silently.
|
||||
|
||||
**A premise NOT to build on — an on-grid value is not uniformly safe from a bare floor.**
|
||||
That claim holds for `0s` only because `0` is exact in binary; it is FALSE in general for
|
||||
a decimal millisecond grid point: `1.007 * 1000 == 1006.9999999999999` (floors to 1006,
|
||||
not 1007), and `4.068 * 1000 == 4067.9999999999995` (floors to 4067, not 4068). If a
|
||||
future fix compensates for a discovered START floor by grid-aligning the extraction — e.g.
|
||||
slicing a buffer from `floor(start_ms)` — and builds that arithmetic on the false premise,
|
||||
a `1.007`-class start would resolve a whole millisecond early: frame count right, content
|
||||
shifted, exactly the silent null-test misalignment this effort exists to catch. Neither
|
||||
live observation above can detect this hazard (both are off-grid sub-millisecond
|
||||
remainders, not grid points) — a `1.007`-class grid point must be measured in the DAW
|
||||
before any extraction logic is built on this premise. `render_window`'s own
|
||||
`isOnMillisecondGrid`/`floorToMilliseconds` already handle this correctly, but only on OUR
|
||||
side of the boundary; that tolerance cannot influence how REAPER itself resolves a value we
|
||||
hand it, which is the open question here, not a closed one.
|
||||
|
||||
**Still open — where the floor lives.** Custom time bounds is one of eight
|
||||
`RENDER_BOUNDSFLAG` modes. Whether the floor sits in that field's own render-time
|
||||
resolution or downstream in the render engine (where no mode escapes it) cannot be
|
||||
answered from the SDK header. `RenderBoundsChannel`
|
||||
(`src/core/capture/render_settings.h`) is the experiment; `docs/VERIFICATION.md`
|
||||
§Capture range and bounds is the one smoke run that settles it.
|
||||
|
||||
**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
|
||||
these refusals — 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.
|
||||
## `capture.cpp` is over the ~600-line ceiling — documented, not split mid-experiment
|
||||
|
||||
**Context.** The bounds-channel live experiment (`RenderBoundsChannel`, this same
|
||||
section above) added the time-selection guard/read-back plumbing and the always-on
|
||||
verdict print to `OfflineRenderBackend::capture`, landing the file at 697 lines against
|
||||
root `CLAUDE.md`'s ~600-line ceiling. The named seam: the drift/verdict instrumentation
|
||||
block (`ScopedTimeSelection`/read-back/drift-report/verdict-print, roughly
|
||||
`capture.cpp:449-655`).
|
||||
|
||||
**Deferred, not silent.** ≈60 of the added lines are temporary probe instrumentation
|
||||
with a known removal date (the experiment closes when `docs/VERIFICATION.md` §Capture
|
||||
range and bounds comes back), and splitting the file mid-experiment risks moving the
|
||||
exact code the smoke run is measuring. Split after the experiment closes, onto the seam
|
||||
named above.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@ 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
|
||||
- [ ] **The millisecond floor — what to expect.** A custom-bounds render is known to floor its window's END to the millisecond and write the floored value back over `RENDER_ENDPOS`. Mechanism, why two `RENDER_BOUNDSFLAG` channels exist, and the two live observations behind this: `src/core/capture/render_settings.h`'s `RenderBoundsChannel` and `docs/TODO.md` "An offline capture can be refused...". **Both live observations started at `0s`, on the grid, so nothing is known about the START edge**
|
||||
- [ ] **The one experiment — does another bounds mode escape the floor?** This build renders on the TIME SELECTION channel (`RENDER_BOUNDSFLAG=2`, window handed over via `GetSet_LoopTimeRange`) instead of custom time bounds. Every capture prints one line beginning `ReaSampler capture -- bounds channel:` — including the two paths that answer before any bounds are judged (unsupported format, no output file), which print `NOT JUDGED` rather than staying silent. Read the verdict: a bare **SHORT**/**LONG** (no tag), or one naming "floored to the millisecond", is the floor's signature — it did not escape this channel. **(WITHIN TOLERANCE)** on a SHORT/LONG is the gate's ordinary ±1-frame edge-convention slack (`render_window.h`), not the floor — don't read it as either result. **EXACT on a window whose END is off the millisecond grid** is the fix — the floor did not reach this channel. **EXACT on a window whose END lands on the grid is NOT conclusive**: the line adds "The END edge is UNTESTED here too" — a floored render prints the identical count by coincidence, so re-run with an off-grid end before trusting EXACT. **NOT JUDGED naming a bounds channel** means that capture answered nothing (tail mode was not None, the render was empty, the render never even reached a bounds check) OR the render source itself derives its own bounds and never consulted the channel — selected-items/razor captures always read this way, so pick a window narrower than the selected item(s) to route through the time-bounded source instead
|
||||
- [ ] **Same run, the START edge.** The verdict line also says whether the run tested the start. Capture a range whose start is NOT a whole millisecond (set View → time unit to Samples, then nudge the selection start off the grid) so the line reads `The START edge IS tested here`. Report that line verbatim — it is the only evidence available for whether the start floors too, and a start floor is the case that would break the null test silently rather than loudly
|
||||
- [ ] **Auto and Manual tail.** Repeat the off-grid-start/off-grid-end capture once with the panel tail toggle at **Auto** and once at **Manual**. Neither is judged against a frame count, so the evidence is the `after render` drift lines: report whether either channel's bounds read back changed. `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, an end floor could be costing Auto/Manual real content with no detector (`checkRenderedBounds` returns immediately for `tailMode != None`) — so also report by ear/measurement whether either tail capture comes up short against the source, not only whether the bounds fields drifted
|
||||
- [ ] Whichever way the experiment lands, the refused render is still kept for diagnosis at `<project folder>/reasampler_refused/` (the refusal line names the path; a failed move leaves it unindexed in the bank folder and says so). Delete the folder when done — nothing in the bank references it
|
||||
|
||||
## Names and channels
|
||||
|
||||
|
||||
Reference in New Issue
Block a user