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, `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 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 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 ## 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 **Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**, transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**,
**Λ → `l`**. So Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into **Λ → `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`, and `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 Λ-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 ## 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 than developer-only; and a hard `unlink` prune is acceptable with a platform-aware
confirmation. **Do not re-litigate them.** 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 **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 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 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 ## Phase Λ — ReaSampler on Linux: both artifacts, shipped
**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented **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 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 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. Λ-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 ### 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 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). 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) Phase Lambda — ReaSampler on Linux (none of the seventeen; a direct request 2026-08-02)
W1 The audits [COMPLETE] W1 The audits [COMPLETE]
T1 build-toolchain-audit ....... L-01..L-10, V1..V10, D1..D7 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.** **(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 > **AMENDED by Ω-F2 (Daniel, 2026-08-03).** The Trigger bullet below is REVERSED and the
the span — `DRAG TO SET LOOP` when no span has ever been set (the pair is parked at the > `LOOP — GATE ONLY` caption is retired; the Gate off-state bullet survives with its styling
last quarter, `defaultLoopBounds`), `LOOP OFF` when a span is retained. The full off-state > corrected. The amended text is what ships — see `docs/PLAN.md` §Phase Ω, Ω-W1-T5.
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 - **Loop off (Gate).** The pair stays **drawn and grabbable**, styled **grey disabled** — not
still draw at full strength today, which is marks that do nothing. **In Trigger the loop the dim teal that shipped, which read as decoration rather than as an off state
pair and the crossfade mark draw Disabled and are not grabbable**, with a dim (*"that was not intuitive to me"*). The centred dim caption in the span survives and still
`LOOP — GATE ONLY` caption in the span. Disabled rather than hidden, because that is the says which off state you are in: `DRAG TO SET LOOP` when no span has ever been set (the pair
established grammar — the editor's Gate segment already refuses and paints Disabled off is parked at the last quarter, `defaultLoopBounds`), `LOOP OFF` when a span is retained. The
the `splineActive` predicate — and because hiding a set loop on a mode flip destroys full off-state machine, and what the explicit enable does to it, is **§6.4**.
information the user put there. The START mark stays fully live in both modes. - **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) ### 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 no loop* **and** *drag here to make one*. The enable takes the first message; the pair keeps
the second. the second.
- **Off, no span ever set** — pair parked at `defaultLoopBounds`, drawn Disabled, caption - **Off, no span ever set** — pair parked at `defaultLoopBounds`, drawn **grey disabled**
`DRAG TO SET LOOP`. Dragging either mark **turns the enable on.** The shipped (Ω-F2; the shipped dim teal was not readable as an off state), caption `DRAG TO SET LOOP`.
drag-to-create gesture survives intact, and it now teaches the enable by demonstration: Dragging either mark **turns the enable on.** The shipped drag-to-create gesture survives
the user drags and watches the chrome toggle light up. intact, and it now teaches the enable by demonstration: the user drags and watches the chrome
- **Off, span retained** — pair drawn Disabled *at its own positions*, caption `LOOP OFF`. toggle light up.
There is nothing to "set," so the drag-me copy would be wrong. Dragging still turns the - **Off, span retained** — pair drawn **grey disabled** *at its own positions*, caption
enable on, by the same rule. `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. - **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 > **A grab implies intent to loop. This rule SURVIVES Ω-F2 and is the reason the Gate off-state
> what keeps the enable from being a gate the user has to remember to open. > 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 #### 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 > **AMENDED by Ω-F2 (Daniel, 2026-08-03).** The MARKS are now hidden in Trigger (§6.3(d) as
chrome-row enable draws Disabled and inert, with its state preserved and restored on the > amended); the chrome-row ENABLE is not. The two halves of the old "same message at two
return to Gate.** It does not clear `hasLoop`, and it does not hide. The enable's Disabled > scales" argument no longer travel together, and the paragraph below says which one survives.
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*. **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` 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. 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 **Three states, not two, and collapsing any pair of them is a defect:** *grey-disabled but
distinction, not an inconsistency**, and the discriminator is who said no: the user's own grabbable* (the Gate off marks — the user's own off, reversible by the very gesture on offer),
off is reversible by the very gesture being offered, while Trigger's refusal comes from the *Disabled and inert* (the chrome enable in Trigger — the engine's refusal, which no drag can
engine and no marker drag can talk it out of it. 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 ### 6.5 Trade-offs and consequences, named