docs: record the millisecond floor as located and closed, and split what stayed open
The retired custom-bounds premise is corrected wherever it was encoded: the tail bit is the time selection's, not custom bounds'.
This commit is contained in:
@@ -942,3 +942,42 @@ own output, the three folder cases, collapsed-mono placement and summing, both m
|
|||||||
transitions waited out past a panel timer tick, undo, name/colour clone, and
|
transitions waited out past a panel timer tick, undo, name/colour clone, and
|
||||||
`GetProjectPathEx` against a non-default recording path — none of it is unit-testable
|
`GetProjectPathEx` against a non-default recording path — none of it is unit-testable
|
||||||
and none has been run.
|
and none has been run.
|
||||||
|
|
||||||
|
### The offline-render millisecond floor — located and closed (ad-hoc)
|
||||||
|
|
||||||
|
Closes the `docs/TODO.md` entry of the same name. REAPER's offline render was
|
||||||
|
intermittently refusing an otherwise-valid capture whenever the requested window's end
|
||||||
|
carried a sub-millisecond remainder — breaking root `CLAUDE.md`'s "exact bounds — no
|
||||||
|
rounding of the requested range" precision invariant.
|
||||||
|
|
||||||
|
**Located, not inferred: the floor lives in the `RENDER_BOUNDSFLAG=0` custom-time-bounds
|
||||||
|
field, not in REAPER's render engine.** Switching the offline render to
|
||||||
|
`RENDER_BOUNDSFLAG=2` (the project's own time selection, driven through
|
||||||
|
`GetSet_LoopTimeRange`) escapes it entirely. Confirmed by two live 48 kHz
|
||||||
|
`TailMode::None` DAW renders, both landing exactly 97627 frames against the window's own
|
||||||
|
count: the first started at the on-grid `0s` and tested only the END edge (a floored end
|
||||||
|
would have printed 97584 — 43 frames short); the second, the decisive run, started at
|
||||||
|
`2.0338983050847457s` and ended at `4.0677966101694913s`, both edges off the
|
||||||
|
millisecond grid, and no millisecond-floored model of either edge alone or both together
|
||||||
|
reproduces 97627. No compensation, trimming, or extraction was needed.
|
||||||
|
`src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection` is now the one
|
||||||
|
narrative home for the mechanism and the measurement; time selection is the only bounds
|
||||||
|
mode the offline render reaches.
|
||||||
|
|
||||||
|
**Two hypotheses this track's originating entry previously carried are disproven, not
|
||||||
|
merely superseded** — both predicted a shortfall tracking the render's CONTENT: that the
|
||||||
|
render bounds itself to the media it can see, and that a trailing-silence trim fires
|
||||||
|
despite `RENDER_NORMALIZE`. The measured cause tracks the WINDOW instead — the exact
|
||||||
|
millisecond-floored count, independent of what the material does. Recorded so neither is
|
||||||
|
re-proposed without a fresh observation.
|
||||||
|
|
||||||
|
**Scaffolding removed.** The experiment's apparatus — a two-position
|
||||||
|
`RenderBoundsChannel` type, a console verdict line, and a three-checkpoint
|
||||||
|
`RENDER_STARTPOS`/`RENDER_ENDPOS` read-back probe — is deleted now that the mechanism is
|
||||||
|
settled.
|
||||||
|
|
||||||
|
**Left open, filed to `docs/TODO.md`:** `renderHonoredBounds`'s one-frame tolerance
|
||||||
|
remains empirical, not proven; and `TailMode::Auto`/`Manual` have no automatic bounds
|
||||||
|
observation at all — `render_bounds_gate` judges `TailMode::None` only, so both modes are
|
||||||
|
fixed by inference (same bounds path, same floor) rather than by measurement, and only
|
||||||
|
the 0-byte gate covers them until a DAW check closes it.
|
||||||
|
|||||||
+63
-49
@@ -710,66 +710,80 @@ refusal reuses `CaptureStatus::BoundsMismatch` rather than minting its own statu
|
|||||||
earlier note here preferred a distinct status, and that preference is unresolved, not
|
earlier note here preferred a distinct status, and that preference is unresolved, not
|
||||||
withdrawn.
|
withdrawn.
|
||||||
|
|
||||||
## An offline capture can be refused for a short render — the millisecond floor
|
## `renderHonoredBounds`'s one-frame tolerance is empirical, not proven
|
||||||
|
|
||||||
**Measured cause (live, 2026-08-02).** Two captures at 48 kHz, `TailMode::None`, matched
|
**Context.** The millisecond-floor defect that motivated this gate is closed
|
||||||
their landed frame count to the frame with their window's END floored to the millisecond:
|
(`docs/COMPLETED.md`), but the gate itself — `render_window.h`'s
|
||||||
`[0s, 1.6551724137931001s)` printed 79440 of 79448 (the console's own `%.17g` read-back),
|
`renderHonoredBounds` — carries a one-frame tolerance that carried through the fix
|
||||||
and `[0s, ~4.067797s)` — the double nearest the six-decimal value the original refusal
|
unchanged and was never itself proven.
|
||||||
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.
|
|
||||||
|
|
||||||
**Disproven by that observation.** The two hypotheses this entry previously carried — that
|
**The wart.** A renderer that resolves the window's two edges by DIFFERENT
|
||||||
the render bounds itself to the media it can see, and that a trailing-silence trim fires
|
conventions can sit two frames from `frameCountFor`'s answer on a
|
||||||
despite `RENDER_NORMALIZE = &(4<<16)` — both predict a shortfall tracking CONTENT. This
|
correctly-honored render. That cannot account for the 8- and 38-frame shortfalls
|
||||||
one tracks the WINDOW: it is the exact ms-floored count, whatever the material does.
|
the floor produced (`docs/COMPLETED.md`), so it was not the cause of those
|
||||||
Neither is the cause. Do not reinstate either without a fresh observation.
|
refusals — but it means a future one- or two-frame refusal may be the gate's own
|
||||||
|
edge convention rather than a real defect.
|
||||||
|
|
||||||
**Still open — the START edge.** Every observation to date started at `0s`. Floor, ceil
|
**Intended fix.** Not proposed. Widening the tolerance is a precision-invariant
|
||||||
and round all leave `0s` alone (it is exactly representable in binary), so nothing is
|
decision, not a bug fix, and was deliberately not taken on speculation.
|
||||||
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.**
|
**Priority / risk.** Low. Nothing to date implicates the tolerance itself;
|
||||||
That claim holds for `0s` only because `0` is exact in binary; it is FALSE in general for
|
recorded so a future narrow refusal is investigated rather than assumed to be
|
||||||
a decimal millisecond grid point: `1.007 * 1000 == 1006.9999999999999` (floors to 1006,
|
the same floor.
|
||||||
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
|
**Done looks like.** Either the tolerance is confirmed correct by a DAW
|
||||||
`RENDER_BOUNDSFLAG` modes. Whether the floor sits in that field's own render-time
|
observation that isolates edge-convention behavior from bounds-floor behavior,
|
||||||
resolution or downstream in the render engine (where no mode escapes it) cannot be
|
or it is widened with the reasoning recorded.
|
||||||
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
|
## `TailMode::Auto` and `Manual` have no automatic bounds observation
|
||||||
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
|
**Context.** `render_bounds_gate.cpp`'s `checkRenderedBounds` returns early for
|
||||||
answer on a correctly-honored render. That cannot account for 38 frames, so it is not
|
anything but `TailMode::None`, so the millisecond-floor fix (`docs/COMPLETED.md`)
|
||||||
these refusals — but it means a future one- or two-frame refusal may be ours, which is why
|
was measured only against `TailMode::None` — Auto and Manual were never
|
||||||
the tolerance was not widened on speculation. Widening it is a precision-invariant
|
observed, before the fix or after it.
|
||||||
decision, not a bug fix.
|
|
||||||
|
**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
|
## `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
|
**Context.** Removing the settled bounds experiment's instrumentation (the console
|
||||||
verdict and the three-checkpoint `RENDER_STARTPOS`/`ENDPOS` read-back) brought the file
|
verdict and the three-checkpoint `RENDER_STARTPOS`/`ENDPOS` read-back) brought the file
|
||||||
from 697 to **622 measured lines**, against root `CLAUDE.md`'s ~600-line ceiling. The
|
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
|
seam that entry originally named is gone with the instrumentation; nothing left in the
|
||||||
file is bisectable without cutting load-bearing why.
|
file is bisectable without cutting load-bearing why.
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,20 @@ snapshot/restore, forces dither and all normalize-postprocessing off, and render
|
|||||||
32-bit float. The tail wires into that existing path — no new render trigger, no
|
32-bit float. The tail wires into that existing path — no new render trigger, no
|
||||||
new backend.
|
new backend.
|
||||||
|
|
||||||
### Bounds are always custom — so the tail bit is always `&1`
|
### Bounds are always the time selection — so the tail bit is always `&4`
|
||||||
|
|
||||||
The backend renders with `RENDER_BOUNDSFLAG = 0` (custom time bounds) for **every**
|
The backend renders with `RENDER_BOUNDSFLAG = 2` (time selection) for **every**
|
||||||
scope and every range type: it sets `RENDER_STARTPOS` / `RENDER_ENDPOS` explicitly
|
scope and every range type: it writes the request's exact seconds into the
|
||||||
from the request's exact seconds (`capture.cpp` ~L352–354). It does **not** use the
|
project's own time selection via `GetSet_LoopTimeRange` (`capture.cpp` ~L470–477;
|
||||||
time-selection / selected-items / regions bounds modes.
|
`RENDER_STARTPOS`/`RENDER_ENDPOS` are also written, as a defensive no-op for a
|
||||||
|
mode-0-only field, but the window itself travels in the time selection). It does
|
||||||
|
**not** use the custom-time-bounds mode (`RENDER_BOUNDSFLAG = 0`) — that mode was
|
||||||
|
tried and retired: DAW observation showed REAPER resolving a custom-bounds window
|
||||||
|
on a whole-millisecond grid AT RENDER TIME, flooring the end and rendering exactly
|
||||||
|
the floored frame count, which silently broke the exact-bounds precision
|
||||||
|
invariant. The time-selection mode does not floor the window. (The one narrative
|
||||||
|
home for that finding is `render_settings.h`'s `kRenderBoundsTimeSelection`; this
|
||||||
|
doc points there rather than retelling it.)
|
||||||
|
|
||||||
`RENDER_TAILFLAG` is a bitmask keyed to the **bounds mode**, not the capture range
|
`RENDER_TAILFLAG` is a bitmask keyed to the **bounds mode**, not the capture range
|
||||||
type (header line 3047):
|
type (header line 3047):
|
||||||
@@ -69,18 +77,20 @@ RENDER_TAILFLAG : &1=custom time bounds, &2=entire project, &4=time selection,
|
|||||||
&32=selected project markers/regions
|
&32=selected project markers/regions
|
||||||
```
|
```
|
||||||
|
|
||||||
Because we always render in custom-time-bounds mode, **the only tail bit that ever
|
Because we always render in time-selection mode, **the only tail bit that ever
|
||||||
applies is `&1`**. There is no per-range-type tail-flag decision to make — a razor
|
applies is `&4`**. There is no per-range-type tail-flag decision to make — a razor
|
||||||
capture, a time-selection capture, and an item capture are all custom-bounds
|
capture, a time-selection capture, and an item capture are all time-selection-bounds
|
||||||
renders under the hood, so all three take `RENDER_TAILFLAG = 1`.
|
renders under the hood, so all three take `RENDER_TAILFLAG = 4`.
|
||||||
|
|
||||||
> **Correction to the framing brief.** The brief asked us to pick a
|
> **Correction to the framing brief.** The brief asked us to pick a
|
||||||
> `RENDER_TAILFLAG` bit *per capture range type* (time selection vs. razor vs. item)
|
> `RENDER_TAILFLAG` bit *per capture range type* (time selection vs. razor vs. item)
|
||||||
> and flagged `&32` as "markers/regions." The header (line 3047) says `&32` =
|
> and flagged `&32` as "markers/regions." The header (line 3047) says `&32` =
|
||||||
> *selected project regions* and `&8` = *all markers/regions* — but neither matters:
|
> *selected project regions* and `&8` = *all markers/regions* — but neither matters:
|
||||||
> our renders are all `RENDER_BOUNDSFLAG = 0`, so the tail bit is `&1` unconditionally.
|
> our renders are all `RENDER_BOUNDSFLAG = 2`, so the tail bit is `&4` unconditionally.
|
||||||
> The existing `kTailFlagCustomBounds = 1.0` constant in `capture.cpp` (~L80) is
|
> The existing `kTailFlagTimeSelection = 4` constant in
|
||||||
> already correct; the field wiring is what's missing.
|
> `src/core/capture/render_settings.h` (the bounds mode's own bit, per bounds mode —
|
||||||
|
> header line 3047) is already correct — it was right from the start; the wording
|
||||||
|
> above it (which had assumed a custom-bounds render) was what was wrong.
|
||||||
|
|
||||||
### Mode 1 — Automatic (default): generous tail + auto-trim to -72 dB
|
### Mode 1 — Automatic (default): generous tail + auto-trim to -72 dB
|
||||||
|
|
||||||
@@ -88,7 +98,7 @@ Set, in addition to the exact `STARTPOS`/`ENDPOS` already driven:
|
|||||||
|
|
||||||
| Setting | Value | Meaning / header ref |
|
| Setting | Value | Meaning / header ref |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `RENDER_TAILFLAG` | `1` | apply tail for custom time bounds (line 3047, `&1`) |
|
| `RENDER_TAILFLAG` | `4` | apply tail for time selection (line 3047, `&4`) |
|
||||||
| `RENDER_TAILMS` | `8000` | the 8 s cap, in ms (line 3048) |
|
| `RENDER_TAILMS` | `8000` | the 8 s cap, in ms (line 3048) |
|
||||||
| `RENDER_NORMALIZE` | `32768` | **only** the trim-ending-silence bit (line 3051, `&32768`) |
|
| `RENDER_NORMALIZE` | `32768` | **only** the trim-ending-silence bit (line 3051, `&32768`) |
|
||||||
| `RENDER_TRIMEND` | `≈ 0.000251` | -72 dB threshold (line 3062; scaling below) |
|
| `RENDER_TRIMEND` | `≈ 0.000251` | -72 dB threshold (line 3062; scaling below) |
|
||||||
@@ -156,7 +166,7 @@ The existing (currently unwired) `CaptureRequest.renderTail` / `tailMs` fields
|
|||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `RENDER_TAILFLAG` | `1` |
|
| `RENDER_TAILFLAG` | `4` |
|
||||||
| `RENDER_TAILMS` | `request.tailMs` (clamped to the 8 s cap — see below) |
|
| `RENDER_TAILMS` | `request.tailMs` (clamped to the 8 s cap — see below) |
|
||||||
| `RENDER_NORMALIZE` | `262144` (`kNormalizeDisableAll`, unchanged) |
|
| `RENDER_NORMALIZE` | `262144` (`kNormalizeDisableAll`, unchanged) |
|
||||||
| `RENDER_TRIMEND` | not set / irrelevant (trim bit is clear) |
|
| `RENDER_TRIMEND` | not set / irrelevant (trim bit is clear) |
|
||||||
@@ -177,9 +187,9 @@ adds a third state, so the wiring is a small enum, not a bool:
|
|||||||
- **None** (default for null-test / verify captures, and the current two-scope
|
- **None** (default for null-test / verify captures, and the current two-scope
|
||||||
action defaults): `RENDER_TAILFLAG = 0`, `RENDER_TAILMS = 0`, normalize =
|
action defaults): `RENDER_TAILFLAG = 0`, `RENDER_TAILMS = 0`, normalize =
|
||||||
disable-all. Exact bounds. Byte-identical to today.
|
disable-all. Exact bounds. Byte-identical to today.
|
||||||
- **Auto** (the new user-facing default for tail-on captures): tailFlag `1`,
|
- **Auto** (the new user-facing default for tail-on captures): tailFlag `4`,
|
||||||
tailMs `8000`, normalize `32768` (surgical trim), trimEnd `0.00025119`.
|
tailMs `8000`, normalize `32768` (surgical trim), trimEnd `0.00025119`.
|
||||||
- **Manual(ms)**: tailFlag `1`, tailMs `clamp(ms, 8000)`, normalize `262144`
|
- **Manual(ms)**: tailFlag `4`, tailMs `clamp(ms, 8000)`, normalize `262144`
|
||||||
(disable-all), no trim.
|
(disable-all), no trim.
|
||||||
|
|
||||||
Recommended shape: replace `bool renderTail` with a `TailMode { None, Auto,
|
Recommended shape: replace `bool renderTail` with a `TailMode { None, Auto,
|
||||||
|
|||||||
Reference in New Issue
Block a user