docs: spec Phase Omega — mode-switch responsiveness and the control surface's second pass

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.
This commit is contained in:
2026-08-03 11:43:28 -04:00
parent 3376f3eaa1
commit 0eb2c67875
2 changed files with 784 additions and 34 deletions
+738 -4
View File
@@ -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<phase>-w<wave>-t<track>-<slug>`. 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.51 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 12 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 T2T5. **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` `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 `td` 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 T1T4. **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
+46 -30
View File
@@ -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