From 0eb2c678754489112e7ac448ae105711381ed459 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 3 Aug 2026 11:43:28 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20spec=20Phase=20Omega=20=E2=80=94=20mode?= =?UTF-8?q?-switch=20responsiveness=20and=20the=20control=20surface's=20se?= =?UTF-8?q?cond=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight tracks across two waves, from Daniel's seven-item list of 2026-08-03; all six forks ruled, none open. Amends instrument-control-surface.md 6.3(d) and 6.4, both reversed by Omega-F2. --- docs/PLAN.md | 742 ++++++++++++++++++++- docs/product/instrument-control-surface.md | 76 ++- 2 files changed, 784 insertions(+), 34 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 53d0d8a..eca1fed 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -13,7 +13,12 @@ seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in `docs/product/bank-package.md`, **and Phase Ρ**, likewise a direct request (Daniel, 2026-08-02), scoped in `docs/product/render-in-place.md`, **and Phase Λ** — the Linux port of both artifacts — likewise a direct request (Daniel, 2026-08-02), scoped in -`docs/product/linux-readiness.md`. +`docs/product/linux-readiness.md`, **and Phase Ω**, which likewise did not come from the +seventeen: it came from a direct list of seven defects and refinements (Daniel, +2026-08-03), and like Ψ it has **no backing product doc of its own** — the seven are +carried verbatim in its phase header below as Ω.1…Ω.7. It does amend two clauses of +`docs/product/instrument-control-surface.md`, which are edited there rather than merely +contradicted here. ## What this doc is, and how it relates to the others @@ -35,10 +40,11 @@ of both artifacts — likewise a direct request (Daniel, 2026-08-02), scoped in **Worktree slug convention:** `p-w-t-`. Greek phase letters transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**, -**Λ → `l`**. So Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into -`pg-w1-t1-knob-interaction-law`, Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`, and +**Λ → `l`**, **Ω → `om`**. So Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into +`pg-w1-t1-knob-interaction-law`, Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`, Λ-W2-T1 into `pl-w2-t1-linux-compile-blockers` (Phase Λ's two audit tracks already ran -under `pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`). +under `pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`), and Ω-W1-T1 +into `pom-w1-t1-mode-switch-responsiveness`. ## Decision state @@ -133,6 +139,27 @@ but every other host must degrade safely; macOS is out; the Linux artifact is sh than developer-only; and a hard `unlink` prune is acceptable with a platform-aware confirmation. **Do not re-litigate them.** +**Phase Ω (added 2026-08-03) opened six [Daniel]-class forks and ALL SIX ARE RULED**, the +same day the phase was framed (Daniel, 2026-08-03): **Ω-F1** the marker-alignment scope — +*"widen to the correct scope"*, the whole overlay↔waveform mapping rather than the START +mark; **Ω-F2** the loop marks — hidden outright in Trigger, drawn **grey-disabled and still +grabbable** in Gate with loop off; **Ω-F3** the single-button toggle rule extends to Filter, +Loop and Mono|Stereo, the last as a **mode selector** with no off state; **Ω-F4** the +active/focus colour is **`AccentPrimary`**, not `AccentHot`; **Ω-F5** the mode-switch symptom +is *"nothing for 2 seconds, then everything"* on **both** entry surfaces; **Ω-F6** the FX +parking **separates UI from processing** — visibility/routing/FX-enable apply synchronously, +per-FX offline/online defers. **Phase Ω therefore carries ZERO unanswered [Daniel]-class +questions and no Ω track is gated on a decision.** Two of the six moved the spec rather than +merely confirming it: Ω-F2 reverses `docs/product/instrument-control-surface.md`'s +"Disabled rather than hidden" marker principle in the Trigger case and retires its +`LOOP — GATE ONLY` caption, and Ω-F6 amends the accepted FX-parking invariant in +`src/shell/view/CLAUDE.md`. Both amendments are named track deliverables, not footnotes. + +**The Λ carve-out above is unchanged and is NOT weakened by this phase.** Λ remains the one +phase in this plan with unanswered [Daniel]-class questions; the enumeration in Λ's paragraph +predates Ω, so read *"scoped to Θ, Ξ, Γ, Ψ, Ε and Ρ"* as **Θ, Ξ, Γ, Ψ, Ε, Ρ and Ω** — seven +phases, all of them fully ruled, and Λ alone excepted. + **Ruling 3 (Daniel, 2026-08-01) — real units at the host boundary.** *"The parameter values exposed to the VST host should be in real units, such that the host automation lanes report usable values."* Satisfied through VST3's **plain-value layer**, not its wire format (which is @@ -240,6 +267,646 @@ touches them. The instrument adds a sixth surface that binds every Θ track: --- +## Phase Ω — Responsiveness, and the control surface's second pass + +**Ships:** A mode switch that draws its result at once instead of two seconds later; a master +meter that moves at 60 FPS instead of two; a deck whose two-state toggles are single buttons +and whose envelope decks are their own overlay targets; loop marks that tell the truth about +the mode they are in; and one coordinate mapping shared by everything that rides the waveform, +so an overlay lands on the frame it names. + +**Consolidates: none of the seventeen.** Phase Ω came from a direct list of seven defects and +refinements (Daniel, 2026-08-03) and has **no backing product doc of its own** — the Ψ +precedent. The seven are its provenance and are recorded here verbatim in substance: + +- **Ω.1** — the ReaSampler 9000 master meter renders at 0.5–1 FPS. *(instrument)* +- **Ω.2** — Filter Mod belongs on the filter's envelope deck, and the Trigger face's knob + spacing is wrong. *(instrument)* +- **Ω.3** — a two-state toggle should be ONE button, not two segments. *(instrument)* +- **Ω.4** — the whole envelope deck should be the overlay's click target, not a 12 px corner + square. *(instrument)* +- **Ω.5** — the loop marks are meaningless in Trigger and unreadable in Gate with loop off. + *(instrument)* +- **Ω.6** — the START marker does not land on the sample position it names — **widened by Ω-F1 + to the overlay↔waveform coordinate mapping as a whole.** *(instrument)* +- **Ω.7** — the arrange⇄design switch takes 1–2 s and shows nothing until it completes. + *(extension)* + +**This phase is small tidy work sequenced BEFORE Λ, and it is not another Λ.** Everything here +is verifiable on the current Windows box except the DAW-observable half, which has one track of +its own (Ω-W2-T3) rather than a wave. It **supersedes nothing** and adds no format, no wire +version, no parameter id, and no new artifact. + +**Two invariant amendments are track deliverables** (the Ψ/Ρ precedent), because two settled +statements become untrue the moment this phase lands: + +- `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 — the marks' "Disabled rather + than hidden" principle and the `LOOP — GATE ONLY` caption, reversed in the Trigger case by + Ω-F2. **Owner: Ω-W1-T5.** *(Amended in that doc as part of this phase's authoring; the track + verifies the code agrees with the amended text.)* +- `src/shell/view/CLAUDE.md`'s FX-parking caveat — today it accepts a "possible load hitch" on + the return to the active mode as the cost of the CPU reclaim. Ω-F6 splits that cost off the + synchronous path entirely. **Owner: Ω-W1-T1.** + +### Rulings — Daniel's, 2026-08-03. Six, settled. + +**Do not re-litigate these.** There is no product doc to index them in; this table is their one +home, and a brief for any Ω track is written against it. + +| Ruling | What it settled | Bound into | +|---|---|---| +| **Ω-F1** | **Widen the marker-alignment work to the correct scope.** The defect is the overlay↔waveform coordinate mapping as a whole, not the START mark — a class defect with four independent causes, inherited by every overlay | Ω-W1-T5 | +| **Ω-F2** | **Trigger ⇒ the loop marks are HIDDEN outright. Gate with loop off ⇒ they are still drawn and still grabbable, styled GREY DISABLED rather than dim teal** — *"that was not intuitive to me."* The drag-to-set-loop gesture survives; `LOOP — GATE ONLY` goes | Ω-W1-T5 | +| **Ω-F3** | **The single-button toggle rule extends to `Filter` and chrome `Loop`** (Primary when active, **dim gray** when off) **and to `Mono\|Stereo`, which is a MODE SELECTOR** — its label reads the current mode and it has **no** dim-gray state | Ω-W1-T4 | +| **Ω-F4** | **The active/focus colour is `AccentPrimary`** — both the active single-button state and the focused envelope deck's border. **Not `AccentHot`**, which stays the hover cue | Ω-W1-T4 | +| **Ω-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 | + +**Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face. +**Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a +hope. + +### Phase-Ω acceptance criteria + +These bind every track in this phase, in addition to the plan-wide set above. + +- **No behaviour changes outside the named defect.** Every track carries that criterion + explicitly. The Gate deck face, the zero-crossing snap, the ballistics constants, the 500 ms + bank/bake poll body and the audio thread are each named below as things that must come out + the other side identical. +- **Nothing is added to any hot path.** No track here touches `process()`, the `peaks` envelope + compute, audition, or the realtime-capture tick. Ω-W1-T3's work is entirely UI-thread paint + cost; Ω-W1-T1's is entirely main-thread REAPER API cost. +- **`reasampler_editor.h` is at 564 lines against the ~600 ceiling** (`docs/TODO.md`, "The + editor's drag state machine has no seam"), and **three W1 tracks add state to it.** The + budget is ~36 lines for the whole wave. **The drag-state seam that TODO entry names is OUT OF + SCOPE for Ω** — a track that finds itself over the ceiling stops and flags rather than + bisecting the header under time pressure, which is exactly the failure that entry predicts. +- **The shell layers this phase touches have no CTest coverage** — `editor_paint_*`, + `editor_input_*` and `shell/view` have no test targets. Every track therefore states, as a + criterion, **what it pushed down into a pure module and asserted there**; "verified by eye in + the DAW" is the fallback, not the plan. +- **Measurement before and after is a deliverable, not a nicety**, for the two performance + items (Ω.1, Ω.7). A track that improves a number it never measured has no acceptance + criterion, only an opinion. + +--- + +### Ω-W1 — The five defects, in parallel + +**Depends on:** nothing. **Five tracks, disjoint at the file level.** Items 2, 3 and 4 are ONE +track on purpose: they land in the same deck modules and the same pinned-geometry test files, +so splitting them means re-deriving `test_deck_groups_measured.cpp` three times. Items 5 and 6 +are one track because they overlap inside `paintWaveform`'s mark-draw loop — one deletes and +gates it, the other changes the `mx` it computes. + +| 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.** `nearestZeroCrossing` + (`waveform_view.cpp:170-199`) 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. +- **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. +- 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. + +--- + +### Ω-W2 — What the measurements say, and what only a DAW can + +**Depends on:** Ω-W1 in full. **Three tracks.** T1 is **measurement-gated** — it may turn out +not to be worth doing, and that is a legitimate outcome. T3's deliverable is a **RECORD, not a +fix** (the Λ-W3 precedent). + +| Track | Owns | +|---|---| +| **T1** `view-topology-cache` | `src/core/view/` — tree / `parentOf` / handle-map memoization | +| **T2** `content-detect-throttle` | `src/shell/panel/panel_input.cpp` — the 30 Hz detector's cadence | +| **T3** `omega-verification-sweep` | `docs/VERIFICATION.md` — the DAW record for all of Ω-W1 | + +**Shared files in the wave:** none. The three tracks touch three disjoint directories. + +#### Ω-W2-T1 — `view-topology-cache` *(measurement-gated on Ω-W1-T1's result)* + +**Goal.** Stop rebuilding the project's folder topology from scratch on every apply — if, after +Ω-W1-T1, that is still a cost worth paying for. + +**Spec:** Ω.7's residual. + +**Surface boundary — owns:** `src/core/view/` (the folder tree, `parentOf`, and the +GUID→handle map's shape). **Does not own:** `shell/view/view.cpp` — Ω-W1-T1 owns it, and this +track lands after. + +**Behavior.** `readFolderEntries` + `buildFolderTree` run on every apply, and `visibleTracks` +walks the tree per parent. **The gate:** Ω-W1-T1 removes the undo-mask cost, the redundant FX +writes and the O(T²) resolve; if the remaining apply time is already inside the frame budget on +the measured project, **this track is not dispatched** and its finding is recorded instead. +`docs/TODO.md` already carries `view_mode_model.cpp` at 815 lines with a named codec seam — do +not take that seam here, and do not grow that file. + +**Acceptance criteria.** +- A before/after measurement on the same project decides the track, and the number is recorded + either way. +- If taken: membership, reconcile and the park/restore round trip are all unchanged, and the + cache is invalidated by every path that can change track topology. +- `core/view` stays pure and CTest-covered. + +**Prerequisites.** Ω-W1-T1 landed and measured. **Discharges:** Ω.7's residual, or records it +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:146`) runs `enumerateLiveGuids` (`:106-135`) +every tick of REAPER's `"timer"` registration (`main.cpp:341`, ~30/sec), regardless of panel +state — O(T + I) REAPER string calls plus ~4(T+I) allocations per tick, on every project. 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 measured before and after. +- The panel's own repaint cadence is unaffected. + +**Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost. + +#### Ω-W2-T3 — `omega-verification-sweep` + +**Goal.** The DAW-observable half of this phase is recorded rather than assumed. + +**Spec:** every `[verify — DAW]` criterion in Ω-W1. + +**Surface boundary — owns:** `docs/VERIFICATION.md` (a Phase-Ω section). **Does not own:** any +source file. **A fix found here opens a track; it is not applied inside this one.** + +**Behavior.** One session, on a project big enough to reproduce Ω.7 (the track records how big +that turned out to be, because that number is itself a finding). Covers: switch-to-first-paint +before and after; two switches in rapid succession; the park/restore round trip after +deferral; Ctrl-Z after a switch; the meter's frame interval and its behaviour under host load; +the Gate deck face against a pre-Ω screenshot; the Trigger face's spacing; every converted +button in all three states; deck-click overlay focus; the marks in Trigger and in +Gate-with-loop-off; and a marker aligned against the waveform at both ends of a sample. + +**Acceptance criteria.** +- Every `[verify — DAW]` mark in Ω-W1 is discharged by a recorded observation or explicitly + carried forward as unrun. **Inspection is not an observation.** +- The two performance numbers are recorded with the machine and project they were measured on. + +**Prerequisites.** All of Ω-W1 landed. **Discharges:** Ω's DAW criteria. + +### Shared files across Ω's waves, named rather than discovered at merge + +All textual adjacency unless marked otherwise. Within Ω-W1 the wave's own table above is the +authority; this one covers the cross-wave and cross-phase cases. + +| File | Tracks | Nature | +|---|---|---| +| `src/shell/view/view.cpp` | Ω-W1-T1 alone | **Deliberately not split.** Already 642 lines and over the ceiling with its seam blocked; T1's new code goes in a new TU beside it rather than into it | +| `src/core/view/*` | Ω-W2-T1 alone | Different wave from T1's shell edits, and gated on them | +| `src/app/CMakeLists.txt` / `src/app/main.cpp` | Ω-W1-T1 | **Cross-phase:** Λ owns other regions of both (Λ-W2-T1/T2/T3, Λ-W4-T3, Λ-W5-T1). Ω lands first; Λ rebases | +| `src/shell/panel/panel_input.cpp` | Ω-W2-T2 | **Cross-phase:** Ρ-W1-T1's `detectNewContent` edit is landed and is the same function — read what shipped, not what was specced | +| `docs/VERIFICATION.md` | Ω-W2-T3 | Λ-W3-T1 writes its own Linux record into a different section | + +--- + ## Phase Λ — ReaSampler on Linux: both artifacts, shipped **Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented @@ -1224,6 +1891,20 @@ proof it exists to give. dispatch and only Λ-F4 gates a wave; see "Decision state" above. It is also the one phase whose acceptance criteria cannot be checked from the current box at all, which is why Λ-W3 exists as a wave rather than as a checklist at the end. +- **All of Phase Ω** (`pom-*`). **Eight tracks across two waves** (W1 five, W2 three), from a + direct list of seven defects and refinements (Daniel, 2026-08-03), not from `TODO-1.0.md`. + Listed here as a block, like Γ, Ψ, Ε, Ρ and Λ; **like Ψ it has no backing product doc** — the + seven are recorded in its phase header as Ω.1…Ω.7, and its six rulings are indexed in the + table there rather than in `docs/product/`. It **supersedes nothing** but **amends two + settled statements**, which is what distinguishes it from the other doc-less phase: Ω-F2 + reverses the marker "Disabled rather than hidden" principle in + `docs/product/instrument-control-surface.md` §6.3(d)/§6.4 for the Trigger case, and Ω-F6 + amends `src/shell/view/CLAUDE.md`'s accepted FX-parking load hitch. Its six [Daniel]-class + forks were opened and ruled the same day it was framed, so no track here is gated on a + decision; see "Decision state" above. **Two `docs/TODO.md` entries constrain it without being + discharged by it:** `reasampler_editor.h`'s ceiling margin (three W1 tracks add state to it, + and the drag-state seam is explicitly out of scope) and `view.cpp`'s blocked split (Ω-W1-T1 + adds a TU beside it rather than growing it). ### Deliberate compressions @@ -1412,6 +2093,59 @@ Phase Rho — Render in place (none of the seventeen; a direct reque that file are landed and are other functions) — the only pre-existing shell files touched. Disjoint from Gamma (core+shell/instrument) and Epsilon (core+shell/package). +Phase Omega — Responsiveness + control surface (none of the seventeen; a direct list of seven) + W1 The five defects, in parallel [5 tracks, file-disjoint] + T1 mode-switch-responsiveness .. Omega.7 structural: undo mask (-1 = UNDO_STATE_ALL, + pulls every FX chunk), unconditional SetOffline, + PreventUIRefresh (used NOWHERE in src today), + compare-before-write, resolve() map, and the + F6 split: UI/routing sync, per-FX park DEFERRED. + New TU beside view.cpp (642 lines, over ceiling). + T2 mode-switch-persist ......... Omega.7 persist: a switch rewrites all SEVEN + ext-state keys when only view_state moved. + T3 meter-rate .................. Omega.1: ONE 500ms timer today -> second timer id + for the meter; 500ms poll body UNCHANGED. + Paint: dirty rect, hoisted bitmap, hero-envelope + cache. 60 FPS target, 30 the measured fallback. + T4 deck-reflow-focus-toggles ... Omega.2 + Omega.3 + Omega.4. Filter Mod -> FILTER + ENV last-in-run (id 1240 FROZEN, annotate only); + run CENTRED at kDeckCellW; single buttons in two + variants (enable = AccentPrimary|dim gray, mode + selector = reads current mode, no off state); + whole env deck = overlay target, radios gone but + DeckRadioDesc survives for MASTER's passive lamp. + [F3 + F4 RULED. Gate face stays PIXEL-IDENTICAL. + COST: the filter tie-line is unreachable after + the move — stated, not discovered] + T5 overlay-mapping ............. Omega.5 + Omega.6. Trigger = marks HIDDEN; Gate + loop-off = GREY DISABLED and still grabbable; + GATE ONLY caption gone, the other two survive. + ONE mapping for both chains — four discrepancies + (2px inset, round vs truncate, closed vs half-open, + half-pixel phase). Overlay-inset route recommended; + draw_kit route is [propose]. Zero-crossing snap + behaviour-identical. Fixtures RE-DERIVED. + [F1 + F2 RULED] + W2 Measurement, and what only a DAW can say [3 tracks] + T1 view-topology-cache ......... core/view memoization [GATED on W1-T1's measurement; + "not worth taking" is a legitimate outcome] + T2 content-detect-throttle ..... the 30/sec enumerate, throttled not weakened + T3 omega-verification-sweep .... the deliverable is a RECORD, not a fix + + Small tidy work sequenced BEFORE Lambda, and deliberately not another Lambda: no format, + no wire version, no parameter id, no new artifact. All six D-rulings SETTLED 2026-08-03 + (F1 widen the mapping scope; F2 hidden in Trigger / grey-disabled in Gate; F3 single + button extends to Filter, Loop, Mono|Stereo; F4 AccentPrimary not AccentHot; F5 both + entry surfaces; F6 separate UI from processing). ZERO open forks. + Two invariant amendments are track deliverables: instrument-control-surface.md 6.3(d) + + 6.4 (T5) and shell/view/CLAUDE.md's FX-parking load hitch (T1). + The solo surface index is EXONERATED (~2T calls, sub-ms) — do not re-litigate it. + reasampler_editor.h has ~36 lines of ceiling margin and THREE W1 tracks add state to it; + the drag-state seam TODO.md names is OUT OF SCOPE — a track over the ceiling stops and + flags. Shared files, named: reasampler_editor.h (T3|T4), editor_paint_waveform.cpp + (T3 envelope cache | T5 marks+mapping), editor_controls.cpp (T4 deckDescs | T5 + waveMarksFor), app/CMakeLists.txt + main.cpp (T1, cross-phase with Lambda). + Phase Lambda — ReaSampler on Linux (none of the seventeen; a direct request 2026-08-02) W1 The audits [COMPLETE] T1 build-toolchain-audit ....... L-01..L-10, V1..V10, D1..D7 diff --git a/docs/product/instrument-control-surface.md b/docs/product/instrument-control-surface.md index 26cf1b8..6e3ec1a 100644 --- a/docs/product/instrument-control-surface.md +++ b/docs/product/instrument-control-surface.md @@ -1156,17 +1156,23 @@ cosmetic gain. **(d) The off-state and the Trigger state get words, not just alpha.** -- **Loop off.** The pair draws in the kit's **Disabled** state with a centred dim caption in - the span — `DRAG TO SET LOOP` when no span has ever been set (the pair is parked at the - last quarter, `defaultLoopBounds`), `LOOP OFF` when a span is retained. The full off-state - machine, and what the explicit enable does to it, is **§6.4**. -- **Trigger mode.** Loop is Gate-only (`resolveLoop` refuses in Trigger) but the markers - still draw at full strength today, which is marks that do nothing. **In Trigger the loop - pair and the crossfade mark draw Disabled and are not grabbable**, with a dim - `LOOP — GATE ONLY` caption in the span. Disabled rather than hidden, because that is the - established grammar — the editor's Gate segment already refuses and paints Disabled off - the `splineActive` predicate — and because hiding a set loop on a mode flip destroys - information the user put there. The START mark stays fully live in both modes. +> **AMENDED by Ω-F2 (Daniel, 2026-08-03).** The Trigger bullet below is REVERSED and the +> `LOOP — GATE ONLY` caption is retired; the Gate off-state bullet survives with its styling +> corrected. The amended text is what ships — see `docs/PLAN.md` §Phase Ω, Ω-W1-T5. + +- **Loop off (Gate).** The pair stays **drawn and grabbable**, styled **grey disabled** — not + the dim teal that shipped, which read as decoration rather than as an off state + (*"that was not intuitive to me"*). The centred dim caption in the span survives and still + says which off state you are in: `DRAG TO SET LOOP` when no span has ever been set (the pair + is parked at the last quarter, `defaultLoopBounds`), `LOOP OFF` when a span is retained. The + full off-state machine, and what the explicit enable does to it, is **§6.4**. +- **Trigger mode.** Loop is Gate-only (`resolveLoop` refuses in Trigger). **In Trigger the loop + pair and the crossfade mark are HIDDEN OUTRIGHT** — not Disabled, not dim: absent, along with + their labels and the caption. *"Trigger mode doesn't work with loop anyway."* The stored span + is untouched and returns with the mode, so nothing the user put there is destroyed — only its + drawing is suppressed where it can mean nothing. **The superseded rule, recorded so it is not + reinstated: the marks once drew Disabled-rather-than-hidden in Trigger, with a dim + `LOOP — GATE ONLY` caption in the span.** The START mark stays fully live in both modes. ### 6.4 The explicit loop enable (Γ-F4, ruled by Daniel 2026-08-01) @@ -1255,34 +1261,44 @@ The parked pair at the last quarter was carrying two messages in one alpha value no loop* **and** *drag here to make one*. The enable takes the first message; the pair keeps the second. -- **Off, no span ever set** — pair parked at `defaultLoopBounds`, drawn Disabled, caption - `DRAG TO SET LOOP`. Dragging either mark **turns the enable on.** The shipped - drag-to-create gesture survives intact, and it now teaches the enable by demonstration: - the user drags and watches the chrome toggle light up. -- **Off, span retained** — pair drawn Disabled *at its own positions*, caption `LOOP OFF`. - There is nothing to "set," so the drag-me copy would be wrong. Dragging still turns the - enable on, by the same rule. +- **Off, no span ever set** — pair parked at `defaultLoopBounds`, drawn **grey disabled** + (Ω-F2; the shipped dim teal was not readable as an off state), caption `DRAG TO SET LOOP`. + Dragging either mark **turns the enable on.** The shipped drag-to-create gesture survives + intact, and it now teaches the enable by demonstration: the user drags and watches the chrome + toggle light up. +- **Off, span retained** — pair drawn **grey disabled** *at its own positions*, caption + `LOOP OFF`. There is nothing to "set," so the drag-me copy would be wrong. Dragging still + turns the enable on, by the same rule. - **On** — full four-mark grammar of §6.3, unchanged. -> **A grab implies intent to loop.** That is the one rule behind both off-states, and it is -> what keeps the enable from being a gate the user has to remember to open. +> **A grab implies intent to loop. This rule SURVIVES Ω-F2 and is the reason the Gate off-state +> marks stay grabbable rather than going grey-and-inert.** It is what keeps the enable from +> being a gate the user has to remember to open. It has nothing to say about Trigger, where +> there is no mark to grab. #### The Trigger case — the enable disables itself, it does not clear itself -Per the Disabled-not-hidden principle already established for the marks: **in Trigger the -chrome-row enable draws Disabled and inert, with its state preserved and restored on the -return to Gate.** It does not clear `hasLoop`, and it does not hide. The enable's Disabled -state and the span's `LOOP — GATE ONLY` caption are the same message delivered at two -scales — the chrome row says *this control is unavailable here*, the span says *why*. +> **AMENDED by Ω-F2 (Daniel, 2026-08-03).** The MARKS are now hidden in Trigger (§6.3(d) as +> amended); the chrome-row ENABLE is not. The two halves of the old "same message at two +> scales" argument no longer travel together, and the paragraph below says which one survives. + +**In Trigger the chrome-row enable draws Disabled and inert, with its state preserved and +restored on the return to Gate.** It does not clear `hasLoop`, and it does not hide — a control +that vanishes from the chrome row costs the user the map of what the instrument has. **The +span's `LOOP — GATE ONLY` caption is retired along with the marks it was centred in**, so the +chrome row now carries the message alone: *this control is unavailable here*. The *why* is the +mode segment sitting beside it. This transitively covers the drawn-EG case: `enforceGateUnavailableWhileDrawn` -(`play_params.h:198-205`) forces Trigger whenever any envelope is drawn, so a spline EG +(`play_params.h`) forces Trigger whenever any envelope is drawn, so a spline EG disables the loop enable through the same predicate rather than through a second rule. -**Disabled-but-grabbable (the off marks) vs. Disabled-and-inert (Trigger) is a deliberate -distinction, not an inconsistency**, and the discriminator is who said no: the user's own -off is reversible by the very gesture being offered, while Trigger's refusal comes from the -engine and no marker drag can talk it out of it. +**Three states, not two, and collapsing any pair of them is a defect:** *grey-disabled but +grabbable* (the Gate off marks — the user's own off, reversible by the very gesture on offer), +*Disabled and inert* (the chrome enable in Trigger — the engine's refusal, which no drag can +talk it out of), and *absent* (the marks in Trigger — a position that can mean nothing in this +mode). The discriminator between the first two is who said no; the discriminator for the third +is whether the thing being drawn could be acted on at all. ### 6.5 Trade-offs and consequences, named