Docs: record Phase Omega's landed work in COMPLETED.md, trim it from PLAN.md
This commit is contained in:
@@ -1661,6 +1661,56 @@ v2 blob keeps the behaviour it had rather than losing every FX state.
|
|||||||
`TrackFX_GetFXGUID` survives a chain reorder while parked — is unverified in the DAW
|
`TrackFX_GetFXGUID` survives a chain reorder while parked — is unverified in the DAW
|
||||||
(SWS issue #802 names a specific way it might not hold).
|
(SWS issue #802 names a specific way it might not hold).
|
||||||
|
|
||||||
|
### Phase Ω — Responsiveness, and the control surface's second pass
|
||||||
|
|
||||||
|
Ω-W1's five tracks landed together, merged to `dev` as `1159d36` (2026-08-03):
|
||||||
|
|
||||||
|
- **Ω-W1-T1 `mode-switch-responsiveness`.** Narrowed the mode-switch undo mask off blanket
|
||||||
|
`UNDO_STATE_ALL`, bracketed the flag-write phase in a `PreventUIRefresh` RAII pair
|
||||||
|
(`UiRefreshHold`), and replaced the linear GUID scan with a hash map (`TrackByGuid`, an
|
||||||
|
`unordered_map`). Also shipped a deferred FX-park queue splitting the synchronous mode-switch
|
||||||
|
apply from per-FX offline/online. A follow-on split of that drain by kind, **Ω-W2-T4**
|
||||||
|
(`70bd29d`), was **reverted** (`2f96dd3`): FX-park state was left stranded across save/reopen,
|
||||||
|
so forced synchronous FX-park drains returned — and with them the mode-switch hitch — until a
|
||||||
|
re-snapshot-after-discard fix lands.
|
||||||
|
- **Ω-W1-T2 `mode-switch-persist`.** A mode switch now writes only the `view_state` ext-state
|
||||||
|
key (`saveViewStateOnly`) instead of the full multi-key session save on every switch.
|
||||||
|
- **Ω-W1-T3 `meter-rate`.** A second timer (`kMeterTimerIntervalMs = 16` ms, ~60 FPS) drains the
|
||||||
|
master meter independently of the 500 ms bank/bake poll (`kSyncTimerIntervalMs`).
|
||||||
|
- **Ω-W1-T4 `deck-reflow-focus-toggles`.** Moved Filter Mod onto the Filter Envelope deck,
|
||||||
|
centred Trigger's reserved knob-cell runs, converted the two-segment toggles (the
|
||||||
|
"Envelope"/"Filter"/"Limiter" enables and chrome "Loop") to single buttons, and made clicking
|
||||||
|
anywhere on an envelope deck focus its overlay.
|
||||||
|
- **Ω-W1-T5 `overlay-mapping`.** Unified the marker/overlay coordinate mapping with the
|
||||||
|
waveform draw chain around one `frameToX`/`OverlayArea`; Trigger now hides the loop marks and
|
||||||
|
caption outright, and Gate-with-loop-off draws them grey-disabled and still grabbable.
|
||||||
|
|
||||||
|
Two Ω-W2 tracks landed the same day. **Ω-W2-T2 `content-detect-throttle`** (`84afd1b`)
|
||||||
|
throttled the panel's new-content enumeration to `kDetectIntervalMs = 500` ms, down from the
|
||||||
|
~30 Hz timer tick it rode before. **Ω-W2-T5 `snap-radius`** (`aa87d81`) bounded
|
||||||
|
`nearestZeroCrossing`'s fan-out to `kMarkerGrabWidth` (5 px) and added Ctrl-defeats-snap on a
|
||||||
|
marker drag; `docs/product/instrument-control-surface.md` §4.1 now documents the gesture.
|
||||||
|
|
||||||
|
**Also landed alongside Ω (2026-08-03), outside the plan:**
|
||||||
|
- `4b19556` **Insert as FX** — a panel button + bindable action loading ReaSampler 9000 onto
|
||||||
|
the selected track with the focused capture.
|
||||||
|
- `098727e` **Panel polish** — a crosshair arrange-drop cue, and Re-capture / Insert Conform
|
||||||
|
moved to the "⋯" overflow menu.
|
||||||
|
- `a5febbd` **Drag-out hand-off fix** — the card drag stays internal while the pointer is over
|
||||||
|
REAPER.
|
||||||
|
|
||||||
|
**Still open, in `docs/PLAN.md`:** Ω-W2-T1 `view-topology-cache` (measurement-gated on
|
||||||
|
Ω-W1-T1's landed result; never dispatched) and Ω-W2-T3 `omega-verification-sweep` (the DAW
|
||||||
|
verification record for Ω-W1; not run).
|
||||||
|
|
||||||
|
**Superseded history.** Ω-W1-T1's deferred FX-park queue, a follow-up snapshot guard
|
||||||
|
(`bd86ede`, 2026-08-05, "refuse to park when the pre-park truth is gone"), and an undo-collapse
|
||||||
|
fix (`03f7fb5`, 2026-08-06) all landed against the synchronous FX-park approach the Aug 3
|
||||||
|
revert left in place. **All of it was superseded on 2026-08-21** by the Design View
|
||||||
|
mode-switch redesign below, which moved per-FX offline/online off the switch path a second
|
||||||
|
time — onto a background reclaimer — and collapsed the undo footprint through the new
|
||||||
|
`src/shell/undo/` module.
|
||||||
|
|
||||||
### Design View mode-switch redesign — FX off the switch path, one undo point
|
### Design View mode-switch redesign — FX off the switch path, one undo point
|
||||||
|
|
||||||
Four waves, merged to `dev` as `da84012`, that took the per-FX offline/online work
|
Four waves, merged to `dev` as `da84012`, that took the per-FX offline/online work
|
||||||
|
|||||||
+30
-515
@@ -392,11 +392,19 @@ cross-reference.
|
|||||||
| **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 |
|
| **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 |
|
||||||
| **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 |
|
| **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 |
|
||||||
|
|
||||||
**SUPERSEDED — Ω-F6 and every deferred-park instruction under Ω-W1-T1 below (the new park-queue TU, its `OnTimer` drain, its `target_sources` line).** The deferral cost a SECOND undo point and could not be made to cost one: REAPER's undo surface has no append-to-point and no block held across a return to the message loop, so per-FX offline/online now runs inline in `applyMode`'s own block and the queue is deleted. The hitch is back on the switch's synchronous path, deliberately. The rest of Ω-W1-T1 (the honest undo mask, compare-before-write, `PreventUIRefresh`, the `resolve` hash map) stands as written. Two of that track's acceptance criteria below are superseded with it: "paints its new state before the park work runs" assumed the split that no longer exists — a switch now parks synchronously before anything paints — and "two rapid switches leave every track in the state the SECOND switch specifies" assumed a queue that could apply both — the synchronous latch REFUSES a switch fired while another is already applying, so a rapid second switch is dropped, not applied.
|
**Doubly superseded — read this as history, not as current behaviour.** Ω-F6's deferred
|
||||||
|
FX-park queue (the park-queue TU, its `OnTimer` drain, its `target_sources` line) shipped
|
||||||
**Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face.
|
with Ω-W1-T1 on 2026-08-03. A follow-on split of that drain by kind, Ω-W2-T4 (`70bd29d`),
|
||||||
**Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a
|
was reverted (`2f96dd3`): FX-park state was left stranded across save/reopen, so forced
|
||||||
hope.
|
synchronous FX-park drains returned — per-FX offline/online back to running inline in
|
||||||
|
`applyMode`'s own block — and with them the mode-switch hitch, until a
|
||||||
|
re-snapshot-after-discard fix lands. A snapshot guard (`bd86ede`, 2026-08-05) and an
|
||||||
|
undo-collapse fix (`03f7fb5`, 2026-08-06) then landed
|
||||||
|
against that synchronous approach. **All of it was superseded again on 2026-08-21** by the
|
||||||
|
Design View mode-switch redesign (`docs/COMPLETED.md`), which moved per-FX offline/online
|
||||||
|
off the switch path a second time — this time onto a background reclaimer — and collapsed
|
||||||
|
the switch's undo footprint through the new `src/shell/undo/` module. Nothing in this
|
||||||
|
paragraph describes current behaviour; see `docs/COMPLETED.md` for what actually ships.
|
||||||
|
|
||||||
### Phase-Ω acceptance criteria
|
### Phase-Ω acceptance criteria
|
||||||
|
|
||||||
@@ -433,458 +441,11 @@ These bind every track in this phase, in addition to the plan-wide set above.
|
|||||||
|
|
||||||
### Ω-W1 — The five defects, in parallel
|
### Ω-W1 — The five defects, in parallel
|
||||||
|
|
||||||
**Depends on:** nothing. **Five tracks, disjoint at the file level.** Items 2, 3 and 4 are ONE
|
**All five tracks have landed** — T1 `mode-switch-responsiveness`, T2 `mode-switch-persist`,
|
||||||
track on purpose: they land in the same deck modules and the same pinned-geometry test files,
|
T3 `meter-rate`, T4 `deck-reflow-focus-toggles`, T5 `overlay-mapping` — merged to `dev` as
|
||||||
so splitting them means re-deriving `test_deck_groups_measured.cpp` three times. Items 5 and 6
|
`1159d36` (2026-08-03). See `docs/COMPLETED.md` for the full narrative. (T1's deferred
|
||||||
are one track because they overlap inside `paintWaveform`'s mark-draw loop — one deletes and
|
FX-park queue was reverted the same day and its downstream mechanics were superseded again
|
||||||
gates it, the other changes the `mx` it computes.
|
on 2026-08-21 — see the rulings note above.)
|
||||||
|
|
||||||
| Track | Owns |
|
|
||||||
|---|---|
|
|
||||||
| **T1** `mode-switch-responsiveness` | `shell/view/view.{h,cpp}` + one new deferred-park TU + its drain in `app/main.cpp` |
|
|
||||||
| **T2** `mode-switch-persist` | `shell/actions/design_view_actions.cpp` + `shell/persist/ext_state_io.cpp` — dirty-key gating |
|
|
||||||
| **T3** `meter-rate` | the editor's timer/paint path + the hero-envelope cache |
|
|
||||||
| **T4** `deck-reflow-focus-toggles` | Ω.2 + Ω.3 + Ω.4 — the deck's composition, layout, paint, input and pinned tests |
|
|
||||||
| **T5** `overlay-mapping` | Ω.5 + Ω.6 — the waveform overlay's marks and its one coordinate mapping |
|
|
||||||
|
|
||||||
**Shared files in the wave, named rather than discovered at merge.** All textual adjacency
|
|
||||||
unless marked otherwise.
|
|
||||||
|
|
||||||
| File | Tracks | Nature |
|
|
||||||
|---|---|---|
|
|
||||||
| `src/shell/instrument/reasampler_editor.h` | T3 (meter clock + timer members), T4 (deck focus state), T5 (nothing new expected) | **The one to watch.** Additive member declarations in different sections of a header with ~36 lines of ceiling margin between them. Whichever lands second rebases AND re-checks the line count |
|
|
||||||
| `src/shell/instrument/editor_paint_waveform.cpp` | T3 (the envelope-cache call site at `paintWaveform`'s bin/`computeEnvelope` block), T5 (the mark, caption and mapping regions below it) | Disjoint regions of one function's body; whichever lands second rebases |
|
|
||||||
| `src/shell/instrument/editor_controls.cpp` | T4 (`faceLayout`'s `deckDescs` build), T5 (`waveMarksFor` / `grabbableMarks`) | Different functions, no shared state |
|
|
||||||
| `src/app/CMakeLists.txt` | T1 (one `target_sources` line for the new park-queue TU) | Ω's only CMake edit. **Cross-phase adjacency:** Λ-W2-T2, Λ-W2-T3, Λ-W4-T3 and Λ-W5-T1 all own regions of this file. Ω lands first; Λ rebases |
|
|
||||||
| `src/app/main.cpp` | T1 (the drain call inside `OnTimer`) | **Cross-phase adjacency:** Λ-W2-T1 owns this file's `REAPERAPI_LoadAPI` failure branch — a different region |
|
|
||||||
|
|
||||||
#### Ω-W1-T1 — `mode-switch-responsiveness`
|
|
||||||
|
|
||||||
**Goal.** A mode switch redraws immediately. The expensive half of the switch still happens; it
|
|
||||||
just stops being the thing the user waits for.
|
|
||||||
|
|
||||||
**Spec:** Ω.7 and rulings Ω-F5, Ω-F6 above; no product doc.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/shell/view/view.cpp` (the `applyMode` body — the undo block,
|
|
||||||
the park/restore loops, the parent-visibility loop, `resolve`), `src/shell/view/view.h`, ONE
|
|
||||||
new TU pair beside them for the deferred queue, one `target_sources` line in
|
|
||||||
`src/app/CMakeLists.txt`, the drain call in `src/app/main.cpp`'s `OnTimer`, and
|
|
||||||
`src/shell/view/CLAUDE.md`'s FX-parking caveat. **Does not own:** `core/view` (that is
|
|
||||||
Ω-W2-T1's), `design_view_actions.cpp` or `ext_state_io.cpp` (T2's), `panel_input.cpp`
|
|
||||||
(Ω-W2-T2's), or `view_solo.cpp`.
|
|
||||||
|
|
||||||
**The solo surface index is EXONERATED.** It costs ~2T extra API calls, sub-millisecond. Say so
|
|
||||||
in the brief so it is not re-litigated: the magnitude is two older O(project) costs that are
|
|
||||||
invisible on a scratch project and quadratic-feeling on a real one.
|
|
||||||
|
|
||||||
**Behavior.**
|
|
||||||
- **The undo mask (`view.cpp:563`).** `Undo_EndBlock2(proj, undoLabel.c_str(), -1)` — `-1` is
|
|
||||||
`UNDO_STATE_ALL` (`reaper_plugin.h:1540`), which includes `UNDO_STATE_FX` (`:1542`). REAPER
|
|
||||||
marshals **every plugin's state chunk in the project** into the undo record, on every switch
|
|
||||||
**and** every tag/untag reapply. The honest mask is
|
|
||||||
`UNDO_STATE_TRACKCFG|UNDO_STATE_ITEMS|UNDO_STATE_MISCCFG`, with `UNDO_STATE_FX` OR'd in only
|
|
||||||
when the park/restore loop actually toggled an offline state this apply. The second
|
|
||||||
`-1` at `:607` (`mintManagedLanes`) is a separate call site — assess it, do not assume it.
|
|
||||||
- **Unconditional `TrackFX_SetOffline`.** `parkFxOffline` (`:190-195`) calls it on **every FX of
|
|
||||||
every parked track**, including tracks already parked and already offline — unloading and
|
|
||||||
re-instantiating plugins for no state change. `restoreFxOffline` (`:200-204`) writes verbatim
|
|
||||||
from the snapshot the same way. Both compare before writing.
|
|
||||||
- **`PreventUIRefresh` is used NOWHERE in `src/`** (verified: zero matches across the tree)
|
|
||||||
though the SDK exports it (`reaper_plugin_functions.h:5583`). ~10T flag writes land with UI
|
|
||||||
refresh live. Bracket the write phase, and keep the existing
|
|
||||||
`TrackList_AdjustWindows(false)` + `UpdateArrange()` as the one deliberate rebuild after the
|
|
||||||
bracket closes. **Balanced pairing is mandatory** — an early return between the two calls
|
|
||||||
leaves REAPER's UI frozen.
|
|
||||||
- **Compare before write.** `applyFlags` (`:182-186`) and the parent-visibility loop
|
|
||||||
(`:521-531`) write unconditionally, so a reapply that changed nothing still pays every write.
|
|
||||||
- **`resolve` is a linear scan** (`:122-128`) called once per parked / restored / parent / lane
|
|
||||||
track — O(T²). `readFolderEntries` (`:103-120`) already builds `handleByGuid` and can hand
|
|
||||||
back a hash map instead of a vector of pairs.
|
|
||||||
- **Ω-F6, and it is the headline.** Split the synchronous apply from the deferred park:
|
|
||||||
visibility, routing and FX-enable apply **inside** `applyMode`; per-FX offline/online is
|
|
||||||
enqueued and drained on a later idle tick. **The spec must state what the queue does when a
|
|
||||||
second switch arrives before the first's park has run** — the answer is that a queue entry is
|
|
||||||
keyed by track GUID and the LATEST intent wins (an enqueued park superseded by a restore
|
|
||||||
cancels, not stacks), because the two are inverses and replaying both is both slower and
|
|
||||||
observably wrong. A project close, a track deletion, or a project switch drains the queue
|
|
||||||
without applying it.
|
|
||||||
- **Where the queue lives.** `src/shell/view/view.cpp` measures **642 lines** today and is
|
|
||||||
already over the ~600 ceiling with its seam blocked (`docs/TODO.md`). The queue is therefore
|
|
||||||
a **new TU beside it**, not more of `view.cpp` — `src/shell/view/` has no `CMakeLists.txt` of
|
|
||||||
its own, so this costs exactly one `target_sources` line in `src/app/CMakeLists.txt` (where
|
|
||||||
`view.cpp` and `view_solo.cpp` are already listed). This is the one structural decision in
|
|
||||||
the track and it moves `view.cpp` toward the ceiling rather than further past it.
|
|
||||||
- **The invariant amendment.** `src/shell/view/CLAUDE.md`'s "Documented caveat" bullet accepts a
|
|
||||||
load hitch on the return to the active mode. After this track the hitch is still real but no
|
|
||||||
longer sits on the switch's synchronous path — the caveat must say so, and must say what the
|
|
||||||
deferral does NOT change (the plugin still re-instantiates; un-persisted internal state is
|
|
||||||
still lost).
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- On a project with enough tracks and FX to reproduce it, the mode switch **paints its new
|
|
||||||
state before the park work runs**, and the wall-clock to first paint is recorded before and
|
|
||||||
after. `[verify — DAW]`, discharged by Ω-W2-T3.
|
|
||||||
- A tag/untag reapply on an unchanged project performs **zero** `TrackFX_SetOffline` calls and
|
|
||||||
**zero** redundant flag writes.
|
|
||||||
- The undo record for one mode switch no longer carries FX state when no offline state changed;
|
|
||||||
**one switch is still one Ctrl-Z**, and Ctrl-Z still restores what it restored before.
|
|
||||||
- The park/restore round trip — the phase's trust anchor — still returns every driven flag to
|
|
||||||
its captured value, including when the deferral means the park lands on a later tick.
|
|
||||||
- Two switches in rapid succession leave every track in the state the SECOND switch specifies,
|
|
||||||
with no track left offline that should be online.
|
|
||||||
- `PreventUIRefresh` is balanced on every exit path from `applyMode`, including the two early
|
|
||||||
returns already there.
|
|
||||||
- `view.cpp` is no longer than it is today.
|
|
||||||
|
|
||||||
**Prerequisites.** None; concurrent with T2–T5. **Discharges:** Ω.7 (the structural half),
|
|
||||||
Ω-F5, Ω-F6.
|
|
||||||
|
|
||||||
#### Ω-W1-T2 — `mode-switch-persist`
|
|
||||||
|
|
||||||
**Goal.** A mode switch stops rewriting the whole session to record that one key changed.
|
|
||||||
|
|
||||||
**Spec:** Ω.7's lower-priority half; ruling Ω-F5 (both entry surfaces).
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/shell/actions/design_view_actions.cpp` (`persistViewState` and
|
|
||||||
the two callers `doActivateMode` / `doToggleMode`) and `src/shell/persist/ext_state_io.cpp`
|
|
||||||
(`saveToActiveProject`). **Does not own:** `view.cpp`, `session.cpp`, or any panel TU.
|
|
||||||
|
|
||||||
**Behavior.**
|
|
||||||
- The panel-click path routes through `Main_OnCommand` → `doActivateMode`
|
|
||||||
(`panel_input.cpp:330-334`), which calls `persistViewState()` →
|
|
||||||
`saveToActiveProject()` — a **seven-key** ext-state write (`ext_state_io.cpp:140-190`: banks,
|
|
||||||
the legacy-key clear, view, tail, owned_files, version stamp, bank generation) plus a
|
|
||||||
`MarkProjectDirty`, every switch, when only `view_state` changed. Serializing the bank book
|
|
||||||
and the tracking ledger is the expensive part and neither moved.
|
|
||||||
- **Gate by dirty key, not by call site.** A per-key dirty flag on the session, or a
|
|
||||||
`saveViewStateOnly` entry point beside the full save — the track picks, and states why. The
|
|
||||||
full save must remain the default for every other caller.
|
|
||||||
- **`saveToActiveProject`'s return contract must not change.** `shell/persist/CLAUDE.md` is
|
|
||||||
explicit: `false` means *no active/saved project and NOTHING was written*, and it may never
|
|
||||||
grow an observational third failure mode. A narrowed write returns on the same rule.
|
|
||||||
- The degraded-ledger suppression (`tracking::ledgerDegraded`) and the version stamp both stay
|
|
||||||
exactly where they are on the full path.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- A mode switch on a saved project writes `view_state` and nothing else; a capture, a bank op
|
|
||||||
and a project save each still write everything they wrote before.
|
|
||||||
- `MarkProjectDirty` still fires on the narrowed path — the state DID change.
|
|
||||||
- Both entry surfaces (the footer segment and the bindable action) take the same path.
|
|
||||||
- The write-count difference is measured and recorded.
|
|
||||||
|
|
||||||
**Prerequisites.** None; concurrent with T1. **Discharges:** Ω.7 (the persist half).
|
|
||||||
|
|
||||||
#### Ω-W1-T3 — `meter-rate`
|
|
||||||
|
|
||||||
**Goal.** The master meter moves at 60 FPS, and a paint costs what one frame should.
|
|
||||||
|
|
||||||
**Spec:** Ω.1; no product doc. `src/shell/instrument/CLAUDE.md` already records that "a
|
|
||||||
meter-rate timer remains a separate change and is not in" — this is that change.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the timer set-up and
|
|
||||||
the `WM_PAINT`/`invalidate` path), `editor_session.cpp` (the meter block currently inside
|
|
||||||
`onSyncTimer`), `editor_paint.cpp` (the paint entry's bitmap and dirty-rect handling), the
|
|
||||||
hero-envelope cache's accessor in `editor_paint_waveform.cpp`, and `reasampler_editor.h`'s
|
|
||||||
meter/timer/cache members. **Does not own:** `meter_ballistics.h`, `master_meter`, the
|
|
||||||
processor, any deck or overlay module.
|
|
||||||
|
|
||||||
**Ballistics need no rework.** Every constant in `core/instrument/engine/meter_ballistics.h` is
|
|
||||||
per-second, and elapsed time is measured rather than assumed — the rate is a UI-cadence
|
|
||||||
problem, not a ballistics one. **Nothing changes on the audio thread.**
|
|
||||||
|
|
||||||
**Behavior.**
|
|
||||||
- **The cause.** There is exactly ONE timer in the tree: `kSyncTimerIntervalMs = 500`
|
|
||||||
(`editor_platform.cpp:33`), the bank/bake change-detection poll. The meter's advance +
|
|
||||||
`invalidate` sit inside `onSyncTimer` (`editor_session.cpp:113-134`), so 2 FPS is the
|
|
||||||
ceiling, and `WM_TIMER` coalescing under host load takes it below that.
|
|
||||||
- **The recommended shape: a SECOND timer id on the same child window**, running only the meter
|
|
||||||
drain, advance and invalidate. **The 500 ms poll body is untouched** — `pollBankSync`,
|
|
||||||
`bakeAvailable`, `resolveBakeHoldNeeded` and the bake run must not speed up (a bridge read
|
|
||||||
plus a bank parse each), and the tick-counted UI decays (`kBakeMessageTicks`,
|
|
||||||
`dropHintTicks_`) keep their meaning for free. **The rejected alternative** is raising the
|
|
||||||
shared interval and converting those decays to elapsed time: it is one fewer timer and it
|
|
||||||
silently shortens two user-visible banners by ~15× if the conversion is missed anywhere.
|
|
||||||
- **`masterBusMeter()` is a CONSUMING drain** (exchange-to-identity) and must keep **exactly
|
|
||||||
ONE caller**, or the two clocks steal windows from each other. Moving it to the meter timer
|
|
||||||
keeps that; leaving a copy behind in `onSyncTimer` breaks it.
|
|
||||||
- **The meter block must stay AHEAD of the in-flight-drag guard.** On its own timer this is
|
|
||||||
structural rather than positional — state it, because the reason (a drag suppresses the
|
|
||||||
reload poll but the bus keeps sounding) survives the move.
|
|
||||||
- **The clock resolution is a real hazard at 16 ms.** Today the elapsed measure is
|
|
||||||
`GetTickCount64` (`editor_session.cpp:114`), whose granularity is ~15.6 ms — at 60 FPS the
|
|
||||||
measured delta quantizes to 0 or 16 and a zero-delta advance does nothing but burn a frame.
|
|
||||||
Move the meter clock to a high-resolution source off the audio thread, and skip the advance
|
|
||||||
on a zero delta rather than passing it in.
|
|
||||||
- **Three paint costs, all of which the higher rate exposes:**
|
|
||||||
1. `invalidate()` (`editor_platform.cpp:65-66`) dirties the **whole client area**
|
|
||||||
(`InvalidateRect(hwnd, nullptr, FALSE)`) and `paint` never reads `ps.rcPaint`
|
|
||||||
(`:149-155`). The meter needs its own rect invalidated and the paint needs to honour the
|
|
||||||
dirty rect.
|
|
||||||
2. `paint` allocates a **full-size `LICE_SysBitmap` per paint** (`editor_paint.cpp:28`).
|
|
||||||
Hoist it to a member resized on `WM_SIZE`, where `thumbCache_.clear()` already lives.
|
|
||||||
3. `paintWaveform` recomputes `computeEnvelope` over the **entire decoded PCM every paint**
|
|
||||||
(`editor_paint_waveform.cpp:204-212`). The decode is cached (`channelPcmFor` /
|
|
||||||
`monoPcmFor`); the hero envelope is not. Cache it keyed on sample id + bin count + lane
|
|
||||||
count, busted by `refreshFromBank` and by a resize — the same discipline `marksCache_` and
|
|
||||||
`thumbCache_` already use.
|
|
||||||
- **Target 60 FPS.** 30 is the fallback **only** if the paint-cost work cannot get there, and
|
|
||||||
choosing it requires the measurement that justifies it.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- The meter visibly tracks transients at the shipped rate; the measured frame interval is
|
|
||||||
recorded, with the rate chosen and why. `[verify — DAW]`, discharged by Ω-W2-T3.
|
|
||||||
- A meter-only frame does **not** repaint the deck, chrome or waveform bands.
|
|
||||||
- No allocation in the steady-state paint path.
|
|
||||||
- With no editor open, nothing changed: the meter timer is bound to the child window exactly as
|
|
||||||
the sync timer is.
|
|
||||||
- The bake affordance, the Hold applicability, the bake message and the drop-hint banner all
|
|
||||||
behave exactly as before — same cadence, same durations.
|
|
||||||
- `masterBusMeter()` has exactly one call site.
|
|
||||||
- The first meter read is still DISCARDED (session history, not a window) and the clip latch is
|
|
||||||
still NOT discarded with it.
|
|
||||||
|
|
||||||
**Prerequisites.** None; concurrent with T1, T2, T4, T5. **Discharges:** Ω.1.
|
|
||||||
|
|
||||||
#### Ω-W1-T4 — `deck-reflow-focus-toggles`
|
|
||||||
|
|
||||||
**Goal.** Filter Mod sits with the envelope it modulates, the Trigger face's knobs sit at their
|
|
||||||
natural pitch, a two-state toggle is one button, and clicking an envelope deck anywhere selects
|
|
||||||
its overlay.
|
|
||||||
|
|
||||||
**Spec:** Ω.2, Ω.3, Ω.4 and rulings Ω-F3, Ω-F4. Deck composition law:
|
|
||||||
`src/core/instrument/ui/CLAUDE.md`'s `knob_deck` and `deck_groups` bullets.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/core/instrument/ui/deck_groups.{h,cpp}`,
|
|
||||||
`knob_deck.{h,cpp}`, `deck_values.cpp` (the toggle commit seam only),
|
|
||||||
`src/core/instrument/ui/sample_chrome.{h,cpp}`, `src/shell/instrument/editor_paint_deck.cpp`,
|
|
||||||
`editor_input_deck.cpp`, `editor_paint_chrome.cpp`, `editor_input_chrome.cpp`,
|
|
||||||
`editor_input.cpp` (the focus-clear arm of the mouse-down dispatch only), `editor_controls.cpp`
|
|
||||||
(`faceLayout`'s `deckDescs` build only), and `tests/test_deck_groups.cpp`,
|
|
||||||
`tests/test_deck_groups_measured.cpp`, `tests/test_knob_deck.cpp`. **Does not own:**
|
|
||||||
`waveform_view`, `envelope_overlay`, `spline_edit`, or any `editor_*_waveform` TU (T5's), and
|
|
||||||
**no `param/` file whatsoever**.
|
|
||||||
|
|
||||||
**Behavior — Ω.2, the move.**
|
|
||||||
- `sampleDeckGroups` (`deck_groups.cpp`) is the sole deck-composition authority. Move
|
|
||||||
`kFilterModAmt` out of FILTER's `cellIds` (`:62-68`) into FILTER ENV's (`:80-89`), **in both
|
|
||||||
faces**, landing **last in the run** — the `kPitchEnvDepth` precedent (`:50-53`).
|
|
||||||
- **NO VST3 parameter-id change.** `kFilterModAmt` keeps its frozen id (`kParamFilterModAmount`
|
|
||||||
= 1240) and its unit; ids are keyed on `DeckParam`, never on deck group. `param_id.h`'s
|
|
||||||
header comment says the blocks run "per deck group in SIGNAL-FLOW order" (`:34`) — that
|
|
||||||
becomes untrue for exactly one row. **Annotate it; never renumber.** The frozen-table rule is
|
|
||||||
the whole reason the annotation is the correct fix.
|
|
||||||
- **This move costs the filter tie-line, and that cost is stated rather than discovered.**
|
|
||||||
Today `test_deck_groups_measured.cpp` pins both rows' filter groups to one right edge (x=640
|
|
||||||
block-relative) — the only reason the row block was widened 1020→1028. After the move the
|
|
||||||
Sound row loses 60 px and the Contour row gains 60, and with the two filter groups then equal
|
|
||||||
in width the tie is **unreachable at any block width** under the space-between law. The
|
|
||||||
alignment goes; the ruling stands. Re-derive that fixture from the new contract, record the
|
|
||||||
loss in it, and **do not change `kDeckRowBlockW` or the editor floor to chase it** — revisiting
|
|
||||||
the floor is a separate proposal, not this track's to make.
|
|
||||||
|
|
||||||
**Behavior — Ω.2, the spacing.**
|
|
||||||
- The defect is a **layout-algorithm artifact, not a constant**. A group's cell run is a
|
|
||||||
reserved width, and a `-1` reserve donates its slot to the surviving cells
|
|
||||||
(`knob_deck.cpp:123-138`), so Trigger's FILTER ENV gets ~100 px cells and AMP ENV ~75 against
|
|
||||||
the standard 60. Gate is uniform at 60 because it has no reserves.
|
|
||||||
- **The law: cells stay at `kDeckCellW`; the run is CENTRED in the reserve.** It is the only
|
|
||||||
candidate satisfying both of Daniel's constraints — group width stays stable across a mode
|
|
||||||
flip (which is why the reserves exist) and knobs keep their natural pitch. Gate is then
|
|
||||||
pixel-identical by construction, because with no reserves the centred run and the divided run
|
|
||||||
are the same run.
|
|
||||||
- **The two rejected laws, recorded so they are not re-proposed:** *(a) drop the reserves and
|
|
||||||
let the group narrow in Trigger* — reflows every neighbour on a mode flip, which is the thing
|
|
||||||
the reserves were introduced to prevent; *(b) keep the division but cap the cell width* —
|
|
||||||
leaves an asymmetric residue at one end and re-introduces the dead-slot look the division law
|
|
||||||
replaced.
|
|
||||||
- A consequence worth stating: once the run is centred, **where a `-1` sits in `cellIds` stops
|
|
||||||
mattering** — only the count does. The reserves become pure width accounting.
|
|
||||||
|
|
||||||
**Behavior — Ω.3, single buttons.**
|
|
||||||
- Today every deck toggle is structurally two segments (`DeckToggleDesc` `knob_deck.h:69-72`,
|
|
||||||
`placeToggle` `knob_deck.cpp:82-94`, `hitTestDeck`'s seg0/seg1 `:259-268`, `drawToggle`
|
|
||||||
`editor_paint_deck.cpp:117-137`). **Two variants replace it:**
|
|
||||||
- **Enable** — one button, label names what it controls, **`AccentPrimary` when active and
|
|
||||||
dim gray when off** (Ω-F3, Ω-F4). Applies to: the envelope enable (`kPitchEnvEnable`,
|
|
||||||
today `Off|On`) → **"Envelope"**; `kFilterEnable` → **"Filter"**; `kLimiterEnable` →
|
|
||||||
**"Limiter"** (it currently names nothing it controls); and the chrome row's
|
|
||||||
`Loop Off|Loop On` → **"Loop"**.
|
|
||||||
- **Mode selector** — one button whose **label reads the current mode**, with **no dim-gray
|
|
||||||
state** (Ω-F3). Applies to: the three `Stg|Spl` env-mode toggles → **"Stage"/"Spline"**,
|
|
||||||
and the chrome row's `Mono|Stereo`.
|
|
||||||
- **A third visual state survives and must not be collapsed into "off":** a control refused
|
|
||||||
by the mode (the chrome Loop enable in Trigger, `kFilterLaw` while the filter is off) draws
|
|
||||||
**Disabled and inert**. Off is live and clickable; Disabled is not.
|
|
||||||
- **Explicitly NOT converted, and that is a boundary rather than an oversight:** `kPlayMode`
|
|
||||||
(`Gate|Trigger`), `kPitchEngine` (`Varisp|Presrv`), `kVoiceMode` (`Poly|Mono`), `kFilterLaw`
|
|
||||||
(`Band|Notch`) and `kMonoTrigger` (`Retrig|Legato`) keep their two-segment form. Daniel named
|
|
||||||
a set; this track ships that set. The mode-selector variant generalizes to them the day he
|
|
||||||
asks.
|
|
||||||
- **Three load-bearing risks the implementation must answer:**
|
|
||||||
1. **A single button carries no `segment`**, so the commit must derive the NEXT state from the
|
|
||||||
current one. `setDeckParam`'s `segment == 1` contract (`deck_values.cpp:135-162`) is the
|
|
||||||
seam; `nextOverlaySelection` (`deck_groups.cpp:298-302`) is the existing precedent for
|
|
||||||
computing a next discrete state purely.
|
|
||||||
2. **`kLimiterEnable`'s split commit must survive VERBATIM** (`editor_input_deck.cpp:67-79`):
|
|
||||||
it commits the parameter set and calls `setLimiterEnabled`, which only ARMS the host's
|
|
||||||
`restartComponent(kLatencyChanged)` for the sync tick to deliver. Nothing on that path may
|
|
||||||
start calling into the host from inside a mouse handler.
|
|
||||||
3. **MASTER has ZERO caption slack** — its caption row (46 + toggle + radio) and its knob row
|
|
||||||
(`kDeckCellW` + `kDeckColumnGap` + `kMeterColumnW`) both measure 130, which is exactly
|
|
||||||
`kDeckSpanningW` − 2·`kDeckGroupPadX`. **So the "Limiter" button must be ≤ 64 px** (what
|
|
||||||
the two 32 px segments occupy today). Wider and `kDeckSpanningW` grows, which pushes
|
|
||||||
`kEditorMinWidth` into the 82 px budget against `kEditorCeilingWidth`. **Every other group
|
|
||||||
absorbs a wider button free** — verified: each is knob-row-bound with 50+ px of caption
|
|
||||||
headroom.
|
|
||||||
- **`hitTestKnobFace` runs no toggle-precedence pass** (`knob_deck.h:217-224`) and is only
|
|
||||||
correct while no toggle rect overlaps a knob circle. Any button that grows must be re-checked
|
|
||||||
against that, or given the same precedence order.
|
|
||||||
|
|
||||||
**Behavior — Ω.4, the deck as the overlay target.**
|
|
||||||
- Remove the three overlay radios from the env groups' descriptors. **`DeckRadioDesc` itself
|
|
||||||
must SURVIVE** — MASTER uses a passive one for the GR lamp (`deck_groups.cpp:140`), and the
|
|
||||||
`passive` skip in `hitTestDeck` (`:255-258`) is what keeps a readout from growing a gesture.
|
|
||||||
- Clicking anywhere in an envelope deck — panel background, knob, or button — focuses that
|
|
||||||
envelope's overlay. The focused deck takes an **`AccentPrimary`** border (Ω-F4). Focus clears
|
|
||||||
when the user clicks a control surface outside it; **the overlay itself is excluded from
|
|
||||||
stealing or clearing focus**, so editing the thing you selected does not deselect it.
|
|
||||||
- **Five design constraints, each of which breaks something if missed:**
|
|
||||||
1. The focus write must land **before** the `hit.kind` switch in `mouseDownDeck`
|
|
||||||
(`editor_input_deck.cpp:31-37`) and must **not consume the click**, or every knob grab and
|
|
||||||
every toggle commit on a focused deck dies.
|
|
||||||
2. **`DeckHit` carries no group id** (`knob_deck.h:190-195`). Either the shell scans
|
|
||||||
`dl.groups` for containment or `DeckHit` gains one — the latter is the smaller lie.
|
|
||||||
3. **`hitTestDeck` returns a miss for a click on group padding** (`knob_deck.cpp:252-278`),
|
|
||||||
which is exactly the "deck panel background" Daniel wants to hit. Containment against
|
|
||||||
`g.box` is the test, not the hit kind.
|
|
||||||
4. **A deck click SETS focus idempotently; it does not toggle to none.** Otherwise every knob
|
|
||||||
tweak on the focused deck unfocuses it. This retires `nextOverlaySelection`'s
|
|
||||||
re-click-clears branch (`deck_groups.cpp:301`) and replaces the radio-id map with a
|
|
||||||
group-id one.
|
|
||||||
5. **Refuse focus changes while a drag is in flight**, and keep `overlayEnv_` transient view
|
|
||||||
state with **no persistence path** — it is what is drawn, not what is played.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- **The Gate deck face is pixel-identical to today**, group boxes and cell rects both, asserted
|
|
||||||
in `test_deck_groups_measured.cpp` — except for FILTER and FILTER ENV, whose widths move by
|
|
||||||
exactly one cell in opposite directions.
|
|
||||||
- Trigger's FILTER ENV and AMP ENV cells measure `kDeckCellW`, centred in their reserves, and
|
|
||||||
each group's box width is **identical in both modes**.
|
|
||||||
- `kFilterModAmt`'s `ParamId` is unchanged; `exposedParams()` returns the same list in the same
|
|
||||||
order; no `param/` file is edited.
|
|
||||||
- Every converted control's commit produces the same audible result as the segment it replaced,
|
|
||||||
including the limiter's latency-restart arm.
|
|
||||||
- `kEditorMinWidth` and `kEditorCeilingWidth` are unchanged, and the 82 px budget is unspent.
|
|
||||||
- Clicking a knob, a button and the bare panel of an envelope deck all focus that overlay; the
|
|
||||||
first two still perform their own action; clicking the overlay does not change focus.
|
|
||||||
- The deck's pure fixtures cover the new composition, the centred run and the focus map — the
|
|
||||||
shell layer is not where any of the three is asserted.
|
|
||||||
|
|
||||||
**Prerequisites.** None; concurrent with T1, T2, T3, T5. **Discharges:** Ω.2, Ω.3, Ω.4, Ω-F3,
|
|
||||||
Ω-F4.
|
|
||||||
|
|
||||||
#### Ω-W1-T5 — `overlay-mapping`
|
|
||||||
|
|
||||||
**Goal.** The loop marks say what the mode means, and every overlay that rides the waveform
|
|
||||||
lands on the frame it names.
|
|
||||||
|
|
||||||
**Spec:** Ω.5, Ω.6 and rulings Ω-F1, Ω-F2; `docs/product/instrument-control-surface.md`
|
|
||||||
§6.3(d) and §6.4 **as amended by Ω-F2**.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/core/instrument/ui/waveform_view.{h,cpp}`,
|
|
||||||
`src/shell/instrument/editor_paint_waveform.cpp` (the mark, caption and overlay regions),
|
|
||||||
`editor_input_waveform.cpp`, `editor_controls.cpp` (`waveMarksFor` / `grabbableMarks` only),
|
|
||||||
`src/core/instrument/ui/spline_edit.h`'s `splineOverlayBox` and `envelope_overlay`, and
|
|
||||||
`tests/test_waveform_view.cpp` + `tests/test_component_geometry.cpp`. **Does not own:** any
|
|
||||||
deck module or `editor_*_deck` TU (T4's), the envelope-cache region of
|
|
||||||
`editor_paint_waveform.cpp` (T3's), or — **without an explicit proposal** —
|
|
||||||
`src/shell/panel/draw_kit.cpp` and `src/core/ui/component_geometry.cpp`.
|
|
||||||
|
|
||||||
**Behavior — Ω.5, the marks.**
|
|
||||||
- **Trigger ⇒ the loop pair and the crossfade mark are HIDDEN entirely.** Not Disabled, not
|
|
||||||
dim: absent. **Gate with loop off ⇒ they are still drawn and still grabbable**, styled **grey
|
|
||||||
disabled** rather than today's dim teal at `kMarkAlphaDisabled`
|
|
||||||
(`editor_paint_waveform.cpp:51-53, 285-305`). The drag-to-set-loop gesture survives, which is
|
|
||||||
the whole reason they stay grabbable.
|
|
||||||
- **The `"LOOP \xe2\x80\x94 GATE ONLY"` caption (`editor_paint_waveform.cpp:254`) is removed
|
|
||||||
unconditionally** — in Trigger there is no span left to centre it in.
|
|
||||||
- **The two sibling captions SURVIVE** (`:255`): `DRAG TO SET LOOP` when the pair is parked and
|
|
||||||
`LOOP OFF` when a span is retained. Their box derives from the marks' span, which still
|
|
||||||
exists in Gate, and they carry the one thing the grey styling cannot — *which* off state you
|
|
||||||
are in, and that dragging will fix it. **This is a [propose]-class call made here rather than
|
|
||||||
left open**; reversing it means deleting information, not styling.
|
|
||||||
- **The seams.** `waveMarksFor`'s `present[]` (`editor_controls.cpp:145`) is the correct place
|
|
||||||
to suppress — it also skips label layout. `grabbableMarks` (`:162`) is the separate
|
|
||||||
grabbability fold and must NOT follow it into hiding for the Gate-off case.
|
|
||||||
- **Warning to the track:** `resolveWaveformClaim`'s smallest-area arbitration and `capAtPoint`'s
|
|
||||||
reverse-order resolve (`editor_input_waveform.cpp:65, 89, 111, 212`) are both sensitive to
|
|
||||||
marks entering and leaving the set — `capAtPoint`'s reverse order exists precisely so a
|
|
||||||
coincident pair stays separable and the crossfade (the one mark with no column) can never be
|
|
||||||
shadowed (`core/instrument/CLAUDE.md`, `waveform_view`). Changing which marks are in the set
|
|
||||||
changes what those two resolve, which is the regression risk in this half of the track.
|
|
||||||
|
|
||||||
**Behavior — Ω.6, the mapping. This is a CLASS defect, not a START-marker instance.**
|
|
||||||
|
|
||||||
Two chains that do not agree, verified this pass:
|
|
||||||
|
|
||||||
| | Marker / overlay chain | Waveform draw chain |
|
|
||||||
|---|---|---|
|
|
||||||
| Area | `waveformOverlayArea` returns the **FULL band, no inset** (`waveform_view.cpp:23-25`) | insets **2 px each side**; `waveformColumnCount` = `width − 4` (`component_geometry.cpp:95-98`), columns start at `box.x + 2` (`draw_kit.cpp:292, 311`) |
|
|
||||||
| Domain | `[0, N]` **closed** (`clampFrame`, `waveform_view.cpp:15-19`) | `[0, N)` **half-open** (`columnMinMax`, `peaks.cpp:69-71`) |
|
|
||||||
| Rounding | **round-half-up** (`frameToX`, `:51-58`) | **truncation** |
|
|
||||||
| Phase | a 2 px marker line centred on an integer (`editor_paint_waveform.cpp:298, :301`) | a 1 px column centred at +0.5 |
|
|
||||||
|
|
||||||
Four independent discrepancies. The 2 px inset dominates — frame 0's marker sits 2 px left of
|
|
||||||
the first column and the error reverses sign toward the end — and the closed domain puts the
|
|
||||||
last frame one pixel outside the band. **Everything riding the overlay inherits all four:** the
|
|
||||||
loop span fill, the crossfade wedges, the staged envelope polyline, and the spline contour —
|
|
||||||
whose `spline_edit.h` guarantee that it uses "the FULL area, no inset, so the drawn contour
|
|
||||||
stays 1:1 with the sample's time axis" **is currently false by 2 px.**
|
|
||||||
|
|
||||||
- **Mandate ONE mapping both chains read**, so the drift cannot recur. Two routes:
|
|
||||||
- **Inset the overlay** (recommended) — instrument-only, and the overlay is already the
|
|
||||||
distinct `OverlayArea` type, so the change has one construction site.
|
|
||||||
- **De-inset the waveform** — `[propose]`, **requires justification before it is taken**: it
|
|
||||||
touches the shared `draw_kit`, which also serves the docked bank panel and the browse-card
|
|
||||||
thumbnails, so its blast radius is three surfaces rather than one.
|
|
||||||
- **The zero-crossing snap must be preserved EXACTLY *by this track*.** `nearestZeroCrossing`
|
|
||||||
(`waveform_view.cpp:181-210`) fans out symmetrically, probing `t−d` before `t+d` so an
|
|
||||||
equidistant tie resolves to the lower frame; it runs on the mono cache at drag time and is
|
|
||||||
exempt for the crossfade handle. **Behaviour-identical after the change** is an acceptance
|
|
||||||
criterion, not an assumption.
|
|
||||||
|
|
||||||
> **Amended, ruling R3 (Daniel, 2026-08-03) — read this as scope, not as a verdict on the
|
|
||||||
> snap.** This clause bound T5 to change the coordinate mapping WITHOUT perturbing the snap,
|
|
||||||
> and **T5 met it: the fan-out it received is the fan-out it returned, and its tie-rule
|
|
||||||
> fixtures pass unmodified.** The snap's behaviour was subsequently changed on purpose by a
|
|
||||||
> DIFFERENT track — **Ω-W2-T5 `snap-radius`**, which bounds the fan-out to
|
|
||||||
> `kMarkerGrabWidth` — so a later reader finding a bounded snap in the tree is looking at
|
|
||||||
> Ω-W2-T5's work, not at a T5 violation. **Bounding which frames are REACHABLE does not
|
|
||||||
> change how a reachable tie RESOLVES**, which is why T5's fixtures survive it untouched.
|
|
||||||
- **One question the track must ANSWER, not dodge:** what "sample-accurate" means when
|
|
||||||
`frames < columns` and one frame spans many pixel columns. A marker must land somewhere
|
|
||||||
inside its own frame's column span; which end, and whether the inverse `xToFrame`
|
|
||||||
(`:60-70`) still round-trips, are the track's to specify.
|
|
||||||
- **The two pinned fixtures are re-DERIVED from the new contract, never re-baselined.**
|
|
||||||
`test_waveform_view.cpp` and `test_component_geometry.cpp` pin the current mapping by
|
|
||||||
construction; a test updated to match whatever the code now prints has stopped being a test.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- One mapping function serves both chains; a second one anywhere in the tree fails review.
|
|
||||||
- Every mark, the loop span fill, both crossfade wedges, the staged polyline and the spline
|
|
||||||
contour land on the same pixel as the waveform column for the frame they name — at frame 0,
|
|
||||||
at the last frame, and at an interior frame, asserted in the pure fixtures.
|
|
||||||
- `frames < columns` and `frames > columns` are both covered.
|
|
||||||
- The zero-crossing snap returns the identical frame for the identical input, ties included.
|
|
||||||
**(Scope criterion on THIS track, discharged. Superseded phase-wide by R3 — the snap's
|
|
||||||
radius bound is Ω-W2-T5's deliberate change, and this track's tie-rule fixtures pass
|
|
||||||
unmodified under it.)**
|
|
||||||
- Trigger shows no loop marks and no loop caption; Gate-with-loop-off shows grey marks that
|
|
||||||
still drag, and dragging one still turns the enable on.
|
|
||||||
- `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 agree with the shipped
|
|
||||||
behaviour.
|
|
||||||
|
|
||||||
**Prerequisites.** None; concurrent with T1–T4. **Discharges:** Ω.5, Ω.6, Ω-F1, Ω-F2.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -894,32 +455,19 @@ stays 1:1 with the sample's time axis" **is currently false by 2 px.**
|
|||||||
not to be worth doing, and that is a legitimate outcome. T3's deliverable is a **RECORD, not a
|
not to be worth doing, and that is a legitimate outcome. T3's deliverable is a **RECORD, not a
|
||||||
fix** (the Λ-W3 precedent).
|
fix** (the Λ-W3 precedent).
|
||||||
|
|
||||||
| Track | Owns |
|
| Track | Owns | Status |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| **T1** `view-topology-cache` | `src/core/view/` — tree / `parentOf` / handle-map memoization |
|
| **T1** `view-topology-cache` | `src/core/view/` — tree / `parentOf` / handle-map memoization | Open — measurement-gated |
|
||||||
| **T2** `content-detect-throttle` | `src/shell/panel/panel_input.cpp` — the 30 Hz detector's cadence |
|
| **T2** `content-detect-throttle` | `src/shell/panel/panel_input.cpp` — the 30 Hz detector's cadence | **Landed**, `84afd1b` (2026-08-03) |
|
||||||
| **T3** `omega-verification-sweep` | `docs/VERIFICATION.md` — the DAW record for all of Ω-W1 |
|
| **T3** `omega-verification-sweep` | `docs/VERIFICATION.md` — the DAW record for all of Ω-W1 | Open — not run |
|
||||||
| **T5** `snap-radius` | `src/core/instrument/ui/waveform_view.{h,cpp}` + the marker-drag input path — **spec'd nowhere; see below** |
|
| **T5** `snap-radius` | `src/core/instrument/ui/waveform_view.{h,cpp}` + the marker-drag input path | **Landed**, `aa87d81` (2026-08-03) |
|
||||||
|
|
||||||
**Shared files in the wave:** none. T1–T3 touch three disjoint directories, and T5 touches a
|
**T2 and T5 have landed** — see `docs/COMPLETED.md` for the full narrative. T5 arrived from
|
||||||
fourth (Ω-W1-T5's surface, which lands first).
|
ruling R3 (Daniel, 2026-08-03, recorded in `docs/product/single-cycle-and-wavetable.md` §7.1)
|
||||||
|
after this phase was written and inherited Ω-W1-T5's file surface rather than getting its own
|
||||||
**Ω-W2-T5 has no spec section here, on purpose.** It arrived from ruling R3 — Daniel,
|
spec section here; there is no Ω-W2-T4. Its documentation gap (Ctrl-defeats-snap on a marker
|
||||||
2026-08-03, recorded in `docs/product/single-cycle-and-wavetable.md` §7.1 and summarized at
|
drag, previously unhomed in `docs/product/instrument-control-surface.md`) has since been
|
||||||
"Flagged for awareness" item 4 above — after this phase was written, and is **committed on
|
closed — that doc's §4.1 now carries the gesture.
|
||||||
branch `omega-w2-t5-snap-radius` (`b7b7e88`)**: it
|
|
||||||
bounds `nearestZeroCrossing`'s fan-out to `kMarkerGrabWidth` (5 px) and adds
|
|
||||||
**Ctrl-defeats-snap** on a marker drag. **Numbered T5 rather than T4 because it inherits
|
|
||||||
Ω-W1-T5's file surface; there is no Ω-W2-T4.** Two things it needs from other owners:
|
|
||||||
|
|
||||||
- **Flagged for doc-keeper:** its record belongs in `docs/COMPLETED.md`. This plan names it
|
|
||||||
only so the phase's own superseded criteria can point at what superseded them.
|
|
||||||
- **Flagged for doc-keeper — a documentation gap, not a defect.** *Ctrl-defeats-snap on a
|
|
||||||
marker drag* is a new **user-visible gesture** with no home in
|
|
||||||
`docs/product/instrument-control-surface.md`, and §4.1's exclusion block there — which
|
|
||||||
excludes waveform markers from the modifier law because *"overloading Shift there would
|
|
||||||
collide with a shipped gesture"* — now reads as a claim that markers carry no modifier at
|
|
||||||
all. That block needs amending and the gesture needs documenting.
|
|
||||||
|
|
||||||
#### Ω-W2-T1 — `view-topology-cache` *(measurement-gated on Ω-W1-T1's result)*
|
#### Ω-W2-T1 — `view-topology-cache` *(measurement-gated on Ω-W1-T1's result)*
|
||||||
|
|
||||||
@@ -949,39 +497,6 @@ not take that seam here, and do not grow that file.
|
|||||||
**Prerequisites.** Ω-W1-T1 landed and measured. **Discharges:** Ω.7's residual, or records it
|
**Prerequisites.** Ω-W1-T1 landed and measured. **Discharges:** Ω.7's residual, or records it
|
||||||
as not worth taking.
|
as not worth taking.
|
||||||
|
|
||||||
#### Ω-W2-T2 — `content-detect-throttle`
|
|
||||||
|
|
||||||
**Goal.** The auto-tag detector stops spending main-thread time 30 times a second on exactly
|
|
||||||
the projects where the switch is slowest.
|
|
||||||
|
|
||||||
**Spec:** Ω.7's third cost.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `src/shell/panel/panel_input.cpp` (`detectNewContent` /
|
|
||||||
`enumerateLiveGuids` and their cadence inside `bankPanelRefresh`). **Does not own:** any other
|
|
||||||
panel TU, `view.cpp`, or the auto-tag RULES — only how often they are asked.
|
|
||||||
|
|
||||||
**Behavior.** `detectNewContent` (`panel_input.cpp:147`) runs `enumerateLiveGuids` (`:106-135`)
|
|
||||||
every tick of REAPER's `"timer"` registration (`main.cpp:354`, ~30/sec), regardless of panel
|
|
||||||
state — O(T + I) REAPER string calls plus roughly 5 allocations per track and 6 per item per
|
|
||||||
tick (a per-element-type count, not the earlier `~4(T+I)` derivation, which undercounted; the
|
|
||||||
ratio the throttle buys is unaffected by the correction). Throttle the ENUMERATION, not the
|
|
||||||
detection semantics: the same GUIDs must still be detected, the first-poll and `reloadPending`
|
|
||||||
re-baseline guards must still fire before any diff, and the lane-minting pass must still run
|
|
||||||
only on a tick that tagged. A slower cadence is a latency change to an invisible background
|
|
||||||
tag, and the track states the chosen interval and why.
|
|
||||||
|
|
||||||
**Acceptance criteria.**
|
|
||||||
- New tracks and items are still auto-tagged into the active mode, and a project reload still
|
|
||||||
re-baselines rather than mass-tagging.
|
|
||||||
- The Ρ-W1-T1 rule survives: an added GUID that already carries a membership record is still
|
|
||||||
dropped, so a render-in-place result is not re-tagged to Design.
|
|
||||||
- The per-tick allocation count is derived, not measured: it needs live REAPER-side track/item
|
|
||||||
counts and a runtime allocation trap neither CTest nor this repo's tooling provides.
|
|
||||||
`[verify — DAW]`.
|
|
||||||
- The panel's own repaint cadence is unaffected.
|
|
||||||
|
|
||||||
**Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost.
|
|
||||||
|
|
||||||
#### Ω-W2-T3 — `omega-verification-sweep`
|
#### Ω-W2-T3 — `omega-verification-sweep`
|
||||||
|
|
||||||
**Goal.** The DAW-observable half of this phase is recorded rather than assumed.
|
**Goal.** The DAW-observable half of this phase is recorded rather than assumed.
|
||||||
|
|||||||
Reference in New Issue
Block a user