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:
+738
-4
@@ -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.5–1 FPS. *(instrument)*
|
||||
- **Ω.2** — Filter Mod belongs on the filter's envelope deck, and the Trigger face's knob
|
||||
spacing is wrong. *(instrument)*
|
||||
- **Ω.3** — a two-state toggle should be ONE button, not two segments. *(instrument)*
|
||||
- **Ω.4** — the whole envelope deck should be the overlay's click target, not a 12 px corner
|
||||
square. *(instrument)*
|
||||
- **Ω.5** — the loop marks are meaningless in Trigger and unreadable in Gate with loop off.
|
||||
*(instrument)*
|
||||
- **Ω.6** — the START marker does not land on the sample position it names — **widened by Ω-F1
|
||||
to the overlay↔waveform coordinate mapping as a whole.** *(instrument)*
|
||||
- **Ω.7** — the arrange⇄design switch takes 1–2 s and shows nothing until it completes.
|
||||
*(extension)*
|
||||
|
||||
**This phase is small tidy work sequenced BEFORE Λ, and it is not another Λ.** Everything here
|
||||
is verifiable on the current Windows box except the DAW-observable half, which has one track of
|
||||
its own (Ω-W2-T3) rather than a wave. It **supersedes nothing** and adds no format, no wire
|
||||
version, no parameter id, and no new artifact.
|
||||
|
||||
**Two invariant amendments are track deliverables** (the Ψ/Ρ precedent), because two settled
|
||||
statements become untrue the moment this phase lands:
|
||||
|
||||
- `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 — the marks' "Disabled rather
|
||||
than hidden" principle and the `LOOP — GATE ONLY` caption, reversed in the Trigger case by
|
||||
Ω-F2. **Owner: Ω-W1-T5.** *(Amended in that doc as part of this phase's authoring; the track
|
||||
verifies the code agrees with the amended text.)*
|
||||
- `src/shell/view/CLAUDE.md`'s FX-parking caveat — today it accepts a "possible load hitch" on
|
||||
the return to the active mode as the cost of the CPU reclaim. Ω-F6 splits that cost off the
|
||||
synchronous path entirely. **Owner: Ω-W1-T1.**
|
||||
|
||||
### Rulings — Daniel's, 2026-08-03. Six, settled.
|
||||
|
||||
**Do not re-litigate these.** There is no product doc to index them in; this table is their one
|
||||
home, and a brief for any Ω track is written against it.
|
||||
|
||||
| Ruling | What it settled | Bound into |
|
||||
|---|---|---|
|
||||
| **Ω-F1** | **Widen the marker-alignment work to the correct scope.** The defect is the overlay↔waveform coordinate mapping as a whole, not the START mark — a class defect with four independent causes, inherited by every overlay | Ω-W1-T5 |
|
||||
| **Ω-F2** | **Trigger ⇒ the loop marks are HIDDEN outright. Gate with loop off ⇒ they are still drawn and still grabbable, styled GREY DISABLED rather than dim teal** — *"that was not intuitive to me."* The drag-to-set-loop gesture survives; `LOOP — GATE ONLY` goes | Ω-W1-T5 |
|
||||
| **Ω-F3** | **The single-button toggle rule extends to `Filter` and chrome `Loop`** (Primary when active, **dim gray** when off) **and to `Mono\|Stereo`, which is a MODE SELECTOR** — its label reads the current mode and it has **no** dim-gray state | Ω-W1-T4 |
|
||||
| **Ω-F4** | **The active/focus colour is `AccentPrimary`** — both the active single-button state and the focused envelope deck's border. **Not `AccentHot`**, which stays the hover cue | Ω-W1-T4 |
|
||||
| **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 |
|
||||
| **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 |
|
||||
|
||||
**Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face.
|
||||
**Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a
|
||||
hope.
|
||||
|
||||
### Phase-Ω acceptance criteria
|
||||
|
||||
These bind every track in this phase, in addition to the plan-wide set above.
|
||||
|
||||
- **No behaviour changes outside the named defect.** Every track carries that criterion
|
||||
explicitly. The Gate deck face, the zero-crossing snap, the ballistics constants, the 500 ms
|
||||
bank/bake poll body and the audio thread are each named below as things that must come out
|
||||
the other side identical.
|
||||
- **Nothing is added to any hot path.** No track here touches `process()`, the `peaks` envelope
|
||||
compute, audition, or the realtime-capture tick. Ω-W1-T3's work is entirely UI-thread paint
|
||||
cost; Ω-W1-T1's is entirely main-thread REAPER API cost.
|
||||
- **`reasampler_editor.h` is at 564 lines against the ~600 ceiling** (`docs/TODO.md`, "The
|
||||
editor's drag state machine has no seam"), and **three W1 tracks add state to it.** The
|
||||
budget is ~36 lines for the whole wave. **The drag-state seam that TODO entry names is OUT OF
|
||||
SCOPE for Ω** — a track that finds itself over the ceiling stops and flags rather than
|
||||
bisecting the header under time pressure, which is exactly the failure that entry predicts.
|
||||
- **The shell layers this phase touches have no CTest coverage** — `editor_paint_*`,
|
||||
`editor_input_*` and `shell/view` have no test targets. Every track therefore states, as a
|
||||
criterion, **what it pushed down into a pure module and asserted there**; "verified by eye in
|
||||
the DAW" is the fallback, not the plan.
|
||||
- **Measurement before and after is a deliverable, not a nicety**, for the two performance
|
||||
items (Ω.1, Ω.7). A track that improves a number it never measured has no acceptance
|
||||
criterion, only an opinion.
|
||||
|
||||
---
|
||||
|
||||
### Ω-W1 — The five defects, in parallel
|
||||
|
||||
**Depends on:** nothing. **Five tracks, disjoint at the file level.** Items 2, 3 and 4 are ONE
|
||||
track on purpose: they land in the same deck modules and the same pinned-geometry test files,
|
||||
so splitting them means re-deriving `test_deck_groups_measured.cpp` three times. Items 5 and 6
|
||||
are one track because they overlap inside `paintWaveform`'s mark-draw loop — one deletes and
|
||||
gates it, the other changes the `mx` it computes.
|
||||
|
||||
| Track | Owns |
|
||||
|---|---|
|
||||
| **T1** `mode-switch-responsiveness` | `shell/view/view.{h,cpp}` + one new deferred-park TU + its drain in `app/main.cpp` |
|
||||
| **T2** `mode-switch-persist` | `shell/actions/design_view_actions.cpp` + `shell/persist/ext_state_io.cpp` — dirty-key gating |
|
||||
| **T3** `meter-rate` | the editor's timer/paint path + the hero-envelope cache |
|
||||
| **T4** `deck-reflow-focus-toggles` | Ω.2 + Ω.3 + Ω.4 — the deck's composition, layout, paint, input and pinned tests |
|
||||
| **T5** `overlay-mapping` | Ω.5 + Ω.6 — the waveform overlay's marks and its one coordinate mapping |
|
||||
|
||||
**Shared files in the wave, named rather than discovered at merge.** All textual adjacency
|
||||
unless marked otherwise.
|
||||
|
||||
| File | Tracks | Nature |
|
||||
|---|---|---|
|
||||
| `src/shell/instrument/reasampler_editor.h` | T3 (meter clock + timer members), T4 (deck focus state), T5 (nothing new expected) | **The one to watch.** Additive member declarations in different sections of a header with ~36 lines of ceiling margin between them. Whichever lands second rebases AND re-checks the line count |
|
||||
| `src/shell/instrument/editor_paint_waveform.cpp` | T3 (the envelope-cache call site at `paintWaveform`'s bin/`computeEnvelope` block), T5 (the mark, caption and mapping regions below it) | Disjoint regions of one function's body; whichever lands second rebases |
|
||||
| `src/shell/instrument/editor_controls.cpp` | T4 (`faceLayout`'s `deckDescs` build), T5 (`waveMarksFor` / `grabbableMarks`) | Different functions, no shared state |
|
||||
| `src/app/CMakeLists.txt` | T1 (one `target_sources` line for the new park-queue TU) | Ω's only CMake edit. **Cross-phase adjacency:** Λ-W2-T2, Λ-W2-T3, Λ-W4-T3 and Λ-W5-T1 all own regions of this file. Ω lands first; Λ rebases |
|
||||
| `src/app/main.cpp` | T1 (the drain call inside `OnTimer`) | **Cross-phase adjacency:** Λ-W2-T1 owns this file's `REAPERAPI_LoadAPI` failure branch — a different region |
|
||||
|
||||
#### Ω-W1-T1 — `mode-switch-responsiveness`
|
||||
|
||||
**Goal.** A mode switch redraws immediately. The expensive half of the switch still happens; it
|
||||
just stops being the thing the user waits for.
|
||||
|
||||
**Spec:** Ω.7 and rulings Ω-F5, Ω-F6 above; no product doc.
|
||||
|
||||
**Surface boundary — owns:** `src/shell/view/view.cpp` (the `applyMode` body — the undo block,
|
||||
the park/restore loops, the parent-visibility loop, `resolve`), `src/shell/view/view.h`, ONE
|
||||
new TU pair beside them for the deferred queue, one `target_sources` line in
|
||||
`src/app/CMakeLists.txt`, the drain call in `src/app/main.cpp`'s `OnTimer`, and
|
||||
`src/shell/view/CLAUDE.md`'s FX-parking caveat. **Does not own:** `core/view` (that is
|
||||
Ω-W2-T1's), `design_view_actions.cpp` or `ext_state_io.cpp` (T2's), `panel_input.cpp`
|
||||
(Ω-W2-T2's), or `view_solo.cpp`.
|
||||
|
||||
**The solo surface index is EXONERATED.** It costs ~2T extra API calls, sub-millisecond. Say so
|
||||
in the brief so it is not re-litigated: the magnitude is two older O(project) costs that are
|
||||
invisible on a scratch project and quadratic-feeling on a real one.
|
||||
|
||||
**Behavior.**
|
||||
- **The undo mask (`view.cpp:563`).** `Undo_EndBlock2(proj, undoLabel.c_str(), -1)` — `-1` is
|
||||
`UNDO_STATE_ALL` (`reaper_plugin.h:1540`), which includes `UNDO_STATE_FX` (`:1542`). REAPER
|
||||
marshals **every plugin's state chunk in the project** into the undo record, on every switch
|
||||
**and** every tag/untag reapply. The honest mask is
|
||||
`UNDO_STATE_TRACKCFG|UNDO_STATE_ITEMS|UNDO_STATE_MISCCFG`, with `UNDO_STATE_FX` OR'd in only
|
||||
when the park/restore loop actually toggled an offline state this apply. The second
|
||||
`-1` at `:607` (`mintManagedLanes`) is a separate call site — assess it, do not assume it.
|
||||
- **Unconditional `TrackFX_SetOffline`.** `parkFxOffline` (`:190-195`) calls it on **every FX of
|
||||
every parked track**, including tracks already parked and already offline — unloading and
|
||||
re-instantiating plugins for no state change. `restoreFxOffline` (`:200-204`) writes verbatim
|
||||
from the snapshot the same way. Both compare before writing.
|
||||
- **`PreventUIRefresh` is used NOWHERE in `src/`** (verified: zero matches across the tree)
|
||||
though the SDK exports it (`reaper_plugin_functions.h:5583`). ~10T flag writes land with UI
|
||||
refresh live. Bracket the write phase, and keep the existing
|
||||
`TrackList_AdjustWindows(false)` + `UpdateArrange()` as the one deliberate rebuild after the
|
||||
bracket closes. **Balanced pairing is mandatory** — an early return between the two calls
|
||||
leaves REAPER's UI frozen.
|
||||
- **Compare before write.** `applyFlags` (`:182-186`) and the parent-visibility loop
|
||||
(`:521-531`) write unconditionally, so a reapply that changed nothing still pays every write.
|
||||
- **`resolve` is a linear scan** (`:122-128`) called once per parked / restored / parent / lane
|
||||
track — O(T²). `readFolderEntries` (`:103-120`) already builds `handleByGuid` and can hand
|
||||
back a hash map instead of a vector of pairs.
|
||||
- **Ω-F6, and it is the headline.** Split the synchronous apply from the deferred park:
|
||||
visibility, routing and FX-enable apply **inside** `applyMode`; per-FX offline/online is
|
||||
enqueued and drained on a later idle tick. **The spec must state what the queue does when a
|
||||
second switch arrives before the first's park has run** — the answer is that a queue entry is
|
||||
keyed by track GUID and the LATEST intent wins (an enqueued park superseded by a restore
|
||||
cancels, not stacks), because the two are inverses and replaying both is both slower and
|
||||
observably wrong. A project close, a track deletion, or a project switch drains the queue
|
||||
without applying it.
|
||||
- **Where the queue lives.** `src/shell/view/view.cpp` measures **642 lines** today and is
|
||||
already over the ~600 ceiling with its seam blocked (`docs/TODO.md`). The queue is therefore
|
||||
a **new TU beside it**, not more of `view.cpp` — `src/shell/view/` has no `CMakeLists.txt` of
|
||||
its own, so this costs exactly one `target_sources` line in `src/app/CMakeLists.txt` (where
|
||||
`view.cpp` and `view_solo.cpp` are already listed). This is the one structural decision in
|
||||
the track and it moves `view.cpp` toward the ceiling rather than further past it.
|
||||
- **The invariant amendment.** `src/shell/view/CLAUDE.md`'s "Documented caveat" bullet accepts a
|
||||
load hitch on the return to the active mode. After this track the hitch is still real but no
|
||||
longer sits on the switch's synchronous path — the caveat must say so, and must say what the
|
||||
deferral does NOT change (the plugin still re-instantiates; un-persisted internal state is
|
||||
still lost).
|
||||
|
||||
**Acceptance criteria.**
|
||||
- On a project with enough tracks and FX to reproduce it, the mode switch **paints its new
|
||||
state before the park work runs**, and the wall-clock to first paint is recorded before and
|
||||
after. `[verify — DAW]`, discharged by Ω-W2-T3.
|
||||
- A tag/untag reapply on an unchanged project performs **zero** `TrackFX_SetOffline` calls and
|
||||
**zero** redundant flag writes.
|
||||
- The undo record for one mode switch no longer carries FX state when no offline state changed;
|
||||
**one switch is still one Ctrl-Z**, and Ctrl-Z still restores what it restored before.
|
||||
- The park/restore round trip — the phase's trust anchor — still returns every driven flag to
|
||||
its captured value, including when the deferral means the park lands on a later tick.
|
||||
- Two switches in rapid succession leave every track in the state the SECOND switch specifies,
|
||||
with no track left offline that should be online.
|
||||
- `PreventUIRefresh` is balanced on every exit path from `applyMode`, including the two early
|
||||
returns already there.
|
||||
- `view.cpp` is no longer than it is today.
|
||||
|
||||
**Prerequisites.** None; concurrent with T2–T5. **Discharges:** Ω.7 (the structural half),
|
||||
Ω-F5, Ω-F6.
|
||||
|
||||
#### Ω-W1-T2 — `mode-switch-persist`
|
||||
|
||||
**Goal.** A mode switch stops rewriting the whole session to record that one key changed.
|
||||
|
||||
**Spec:** Ω.7's lower-priority half; ruling Ω-F5 (both entry surfaces).
|
||||
|
||||
**Surface boundary — owns:** `src/shell/actions/design_view_actions.cpp` (`persistViewState` and
|
||||
the two callers `doActivateMode` / `doToggleMode`) and `src/shell/persist/ext_state_io.cpp`
|
||||
(`saveToActiveProject`). **Does not own:** `view.cpp`, `session.cpp`, or any panel TU.
|
||||
|
||||
**Behavior.**
|
||||
- The panel-click path routes through `Main_OnCommand` → `doActivateMode`
|
||||
(`panel_input.cpp:330-334`), which calls `persistViewState()` →
|
||||
`saveToActiveProject()` — a **seven-key** ext-state write (`ext_state_io.cpp:140-190`: banks,
|
||||
the legacy-key clear, view, tail, owned_files, version stamp, bank generation) plus a
|
||||
`MarkProjectDirty`, every switch, when only `view_state` changed. Serializing the bank book
|
||||
and the tracking ledger is the expensive part and neither moved.
|
||||
- **Gate by dirty key, not by call site.** A per-key dirty flag on the session, or a
|
||||
`saveViewStateOnly` entry point beside the full save — the track picks, and states why. The
|
||||
full save must remain the default for every other caller.
|
||||
- **`saveToActiveProject`'s return contract must not change.** `shell/persist/CLAUDE.md` is
|
||||
explicit: `false` means *no active/saved project and NOTHING was written*, and it may never
|
||||
grow an observational third failure mode. A narrowed write returns on the same rule.
|
||||
- The degraded-ledger suppression (`tracking::ledgerDegraded`) and the version stamp both stay
|
||||
exactly where they are on the full path.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- A mode switch on a saved project writes `view_state` and nothing else; a capture, a bank op
|
||||
and a project save each still write everything they wrote before.
|
||||
- `MarkProjectDirty` still fires on the narrowed path — the state DID change.
|
||||
- Both entry surfaces (the footer segment and the bindable action) take the same path.
|
||||
- The write-count difference is measured and recorded.
|
||||
|
||||
**Prerequisites.** None; concurrent with T1. **Discharges:** Ω.7 (the persist half).
|
||||
|
||||
#### Ω-W1-T3 — `meter-rate`
|
||||
|
||||
**Goal.** The master meter moves at 60 FPS, and a paint costs what one frame should.
|
||||
|
||||
**Spec:** Ω.1; no product doc. `src/shell/instrument/CLAUDE.md` already records that "a
|
||||
meter-rate timer remains a separate change and is not in" — this is that change.
|
||||
|
||||
**Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the timer set-up and
|
||||
the `WM_PAINT`/`invalidate` path), `editor_session.cpp` (the meter block currently inside
|
||||
`onSyncTimer`), `editor_paint.cpp` (the paint entry's bitmap and dirty-rect handling), the
|
||||
hero-envelope cache's accessor in `editor_paint_waveform.cpp`, and `reasampler_editor.h`'s
|
||||
meter/timer/cache members. **Does not own:** `meter_ballistics.h`, `master_meter`, the
|
||||
processor, any deck or overlay module.
|
||||
|
||||
**Ballistics need no rework.** Every constant in `core/instrument/engine/meter_ballistics.h` is
|
||||
per-second, and elapsed time is measured rather than assumed — the rate is a UI-cadence
|
||||
problem, not a ballistics one. **Nothing changes on the audio thread.**
|
||||
|
||||
**Behavior.**
|
||||
- **The cause.** There is exactly ONE timer in the tree: `kSyncTimerIntervalMs = 500`
|
||||
(`editor_platform.cpp:33`), the bank/bake change-detection poll. The meter's advance +
|
||||
`invalidate` sit inside `onSyncTimer` (`editor_session.cpp:113-134`), so 2 FPS is the
|
||||
ceiling, and `WM_TIMER` coalescing under host load takes it below that.
|
||||
- **The recommended shape: a SECOND timer id on the same child window**, running only the meter
|
||||
drain, advance and invalidate. **The 500 ms poll body is untouched** — `pollBankSync`,
|
||||
`bakeAvailable`, `resolveBakeHoldNeeded` and the bake run must not speed up (a bridge read
|
||||
plus a bank parse each), and the tick-counted UI decays (`kBakeMessageTicks`,
|
||||
`dropHintTicks_`) keep their meaning for free. **The rejected alternative** is raising the
|
||||
shared interval and converting those decays to elapsed time: it is one fewer timer and it
|
||||
silently shortens two user-visible banners by ~15× if the conversion is missed anywhere.
|
||||
- **`masterBusMeter()` is a CONSUMING drain** (exchange-to-identity) and must keep **exactly
|
||||
ONE caller**, or the two clocks steal windows from each other. Moving it to the meter timer
|
||||
keeps that; leaving a copy behind in `onSyncTimer` breaks it.
|
||||
- **The meter block must stay AHEAD of the in-flight-drag guard.** On its own timer this is
|
||||
structural rather than positional — state it, because the reason (a drag suppresses the
|
||||
reload poll but the bus keeps sounding) survives the move.
|
||||
- **The clock resolution is a real hazard at 16 ms.** Today the elapsed measure is
|
||||
`GetTickCount64` (`editor_session.cpp:114`), whose granularity is ~15.6 ms — at 60 FPS the
|
||||
measured delta quantizes to 0 or 16 and a zero-delta advance does nothing but burn a frame.
|
||||
Move the meter clock to a high-resolution source off the audio thread, and skip the advance
|
||||
on a zero delta rather than passing it in.
|
||||
- **Three paint costs, all of which the higher rate exposes:**
|
||||
1. `invalidate()` (`editor_platform.cpp:65-66`) dirties the **whole client area**
|
||||
(`InvalidateRect(hwnd, nullptr, FALSE)`) and `paint` never reads `ps.rcPaint`
|
||||
(`:149-155`). The meter needs its own rect invalidated and the paint needs to honour the
|
||||
dirty rect.
|
||||
2. `paint` allocates a **full-size `LICE_SysBitmap` per paint** (`editor_paint.cpp:28`).
|
||||
Hoist it to a member resized on `WM_SIZE`, where `thumbCache_.clear()` already lives.
|
||||
3. `paintWaveform` recomputes `computeEnvelope` over the **entire decoded PCM every paint**
|
||||
(`editor_paint_waveform.cpp:204-212`). The decode is cached (`channelPcmFor` /
|
||||
`monoPcmFor`); the hero envelope is not. Cache it keyed on sample id + bin count + lane
|
||||
count, busted by `refreshFromBank` and by a resize — the same discipline `marksCache_` and
|
||||
`thumbCache_` already use.
|
||||
- **Target 60 FPS.** 30 is the fallback **only** if the paint-cost work cannot get there, and
|
||||
choosing it requires the measurement that justifies it.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- The meter visibly tracks transients at the shipped rate; the measured frame interval is
|
||||
recorded, with the rate chosen and why. `[verify — DAW]`, discharged by Ω-W2-T3.
|
||||
- A meter-only frame does **not** repaint the deck, chrome or waveform bands.
|
||||
- No allocation in the steady-state paint path.
|
||||
- With no editor open, nothing changed: the meter timer is bound to the child window exactly as
|
||||
the sync timer is.
|
||||
- The bake affordance, the Hold applicability, the bake message and the drop-hint banner all
|
||||
behave exactly as before — same cadence, same durations.
|
||||
- `masterBusMeter()` has exactly one call site.
|
||||
- The first meter read is still DISCARDED (session history, not a window) and the clip latch is
|
||||
still NOT discarded with it.
|
||||
|
||||
**Prerequisites.** None; concurrent with T1, T2, T4, T5. **Discharges:** Ω.1.
|
||||
|
||||
#### Ω-W1-T4 — `deck-reflow-focus-toggles`
|
||||
|
||||
**Goal.** Filter Mod sits with the envelope it modulates, the Trigger face's knobs sit at their
|
||||
natural pitch, a two-state toggle is one button, and clicking an envelope deck anywhere selects
|
||||
its overlay.
|
||||
|
||||
**Spec:** Ω.2, Ω.3, Ω.4 and rulings Ω-F3, Ω-F4. Deck composition law:
|
||||
`src/core/instrument/ui/CLAUDE.md`'s `knob_deck` and `deck_groups` bullets.
|
||||
|
||||
**Surface boundary — owns:** `src/core/instrument/ui/deck_groups.{h,cpp}`,
|
||||
`knob_deck.{h,cpp}`, `deck_values.cpp` (the toggle commit seam only),
|
||||
`src/core/instrument/ui/sample_chrome.{h,cpp}`, `src/shell/instrument/editor_paint_deck.cpp`,
|
||||
`editor_input_deck.cpp`, `editor_paint_chrome.cpp`, `editor_input_chrome.cpp`,
|
||||
`editor_input.cpp` (the focus-clear arm of the mouse-down dispatch only), `editor_controls.cpp`
|
||||
(`faceLayout`'s `deckDescs` build only), and `tests/test_deck_groups.cpp`,
|
||||
`tests/test_deck_groups_measured.cpp`, `tests/test_knob_deck.cpp`. **Does not own:**
|
||||
`waveform_view`, `envelope_overlay`, `spline_edit`, or any `editor_*_waveform` TU (T5's), and
|
||||
**no `param/` file whatsoever**.
|
||||
|
||||
**Behavior — Ω.2, the move.**
|
||||
- `sampleDeckGroups` (`deck_groups.cpp`) is the sole deck-composition authority. Move
|
||||
`kFilterModAmt` out of FILTER's `cellIds` (`:62-68`) into FILTER ENV's (`:80-89`), **in both
|
||||
faces**, landing **last in the run** — the `kPitchEnvDepth` precedent (`:50-53`).
|
||||
- **NO VST3 parameter-id change.** `kFilterModAmt` keeps its frozen id (`kParamFilterModAmount`
|
||||
= 1240) and its unit; ids are keyed on `DeckParam`, never on deck group. `param_id.h`'s
|
||||
header comment says the blocks run "per deck group in SIGNAL-FLOW order" (`:34`) — that
|
||||
becomes untrue for exactly one row. **Annotate it; never renumber.** The frozen-table rule is
|
||||
the whole reason the annotation is the correct fix.
|
||||
- **This move costs the filter tie-line, and that cost is stated rather than discovered.**
|
||||
Today `test_deck_groups_measured.cpp` pins both rows' filter groups to one right edge (x=640
|
||||
block-relative) — the only reason the row block was widened 1020→1028. After the move the
|
||||
Sound row loses 60 px and the Contour row gains 60, and with the two filter groups then equal
|
||||
in width the tie is **unreachable at any block width** under the space-between law. The
|
||||
alignment goes; the ruling stands. Re-derive that fixture from the new contract, record the
|
||||
loss in it, and **do not change `kDeckRowBlockW` or the editor floor to chase it** — revisiting
|
||||
the floor is a separate proposal, not this track's to make.
|
||||
|
||||
**Behavior — Ω.2, the spacing.**
|
||||
- The defect is a **layout-algorithm artifact, not a constant**. A group's cell run is a
|
||||
reserved width, and a `-1` reserve donates its slot to the surviving cells
|
||||
(`knob_deck.cpp:123-138`), so Trigger's FILTER ENV gets ~100 px cells and AMP ENV ~75 against
|
||||
the standard 60. Gate is uniform at 60 because it has no reserves.
|
||||
- **The law: cells stay at `kDeckCellW`; the run is CENTRED in the reserve.** It is the only
|
||||
candidate satisfying both of Daniel's constraints — group width stays stable across a mode
|
||||
flip (which is why the reserves exist) and knobs keep their natural pitch. Gate is then
|
||||
pixel-identical by construction, because with no reserves the centred run and the divided run
|
||||
are the same run.
|
||||
- **The two rejected laws, recorded so they are not re-proposed:** *(a) drop the reserves and
|
||||
let the group narrow in Trigger* — reflows every neighbour on a mode flip, which is the thing
|
||||
the reserves were introduced to prevent; *(b) keep the division but cap the cell width* —
|
||||
leaves an asymmetric residue at one end and re-introduces the dead-slot look the division law
|
||||
replaced.
|
||||
- A consequence worth stating: once the run is centred, **where a `-1` sits in `cellIds` stops
|
||||
mattering** — only the count does. The reserves become pure width accounting.
|
||||
|
||||
**Behavior — Ω.3, single buttons.**
|
||||
- Today every deck toggle is structurally two segments (`DeckToggleDesc` `knob_deck.h:69-72`,
|
||||
`placeToggle` `knob_deck.cpp:82-94`, `hitTestDeck`'s seg0/seg1 `:259-268`, `drawToggle`
|
||||
`editor_paint_deck.cpp:117-137`). **Two variants replace it:**
|
||||
- **Enable** — one button, label names what it controls, **`AccentPrimary` when active and
|
||||
dim gray when off** (Ω-F3, Ω-F4). Applies to: the envelope enable (`kPitchEnvEnable`,
|
||||
today `Off|On`) → **"Envelope"**; `kFilterEnable` → **"Filter"**; `kLimiterEnable` →
|
||||
**"Limiter"** (it currently names nothing it controls); and the chrome row's
|
||||
`Loop Off|Loop On` → **"Loop"**.
|
||||
- **Mode selector** — one button whose **label reads the current mode**, with **no dim-gray
|
||||
state** (Ω-F3). Applies to: the three `Stg|Spl` env-mode toggles → **"Stage"/"Spline"**,
|
||||
and the chrome row's `Mono|Stereo`.
|
||||
- **A third visual state survives and must not be collapsed into "off":** a control refused
|
||||
by the mode (the chrome Loop enable in Trigger, `kFilterLaw` while the filter is off) draws
|
||||
**Disabled and inert**. Off is live and clickable; Disabled is not.
|
||||
- **Explicitly NOT converted, and that is a boundary rather than an oversight:** `kPlayMode`
|
||||
(`Gate|Trigger`), `kPitchEngine` (`Varisp|Presrv`), `kVoiceMode` (`Poly|Mono`), `kFilterLaw`
|
||||
(`Band|Notch`) and `kMonoTrigger` (`Retrig|Legato`) keep their two-segment form. Daniel named
|
||||
a set; this track ships that set. The mode-selector variant generalizes to them the day he
|
||||
asks.
|
||||
- **Three load-bearing risks the implementation must answer:**
|
||||
1. **A single button carries no `segment`**, so the commit must derive the NEXT state from the
|
||||
current one. `setDeckParam`'s `segment == 1` contract (`deck_values.cpp:135-162`) is the
|
||||
seam; `nextOverlaySelection` (`deck_groups.cpp:298-302`) is the existing precedent for
|
||||
computing a next discrete state purely.
|
||||
2. **`kLimiterEnable`'s split commit must survive VERBATIM** (`editor_input_deck.cpp:67-79`):
|
||||
it commits the parameter set and calls `setLimiterEnabled`, which only ARMS the host's
|
||||
`restartComponent(kLatencyChanged)` for the sync tick to deliver. Nothing on that path may
|
||||
start calling into the host from inside a mouse handler.
|
||||
3. **MASTER has ZERO caption slack** — its caption row (46 + toggle + radio) and its knob row
|
||||
(`kDeckCellW` + `kDeckColumnGap` + `kMeterColumnW`) both measure 130, which is exactly
|
||||
`kDeckSpanningW` − 2·`kDeckGroupPadX`. **So the "Limiter" button must be ≤ 64 px** (what
|
||||
the two 32 px segments occupy today). Wider and `kDeckSpanningW` grows, which pushes
|
||||
`kEditorMinWidth` into the 82 px budget against `kEditorCeilingWidth`. **Every other group
|
||||
absorbs a wider button free** — verified: each is knob-row-bound with 50+ px of caption
|
||||
headroom.
|
||||
- **`hitTestKnobFace` runs no toggle-precedence pass** (`knob_deck.h:217-224`) and is only
|
||||
correct while no toggle rect overlaps a knob circle. Any button that grows must be re-checked
|
||||
against that, or given the same precedence order.
|
||||
|
||||
**Behavior — Ω.4, the deck as the overlay target.**
|
||||
- Remove the three overlay radios from the env groups' descriptors. **`DeckRadioDesc` itself
|
||||
must SURVIVE** — MASTER uses a passive one for the GR lamp (`deck_groups.cpp:140`), and the
|
||||
`passive` skip in `hitTestDeck` (`:255-258`) is what keeps a readout from growing a gesture.
|
||||
- Clicking anywhere in an envelope deck — panel background, knob, or button — focuses that
|
||||
envelope's overlay. The focused deck takes an **`AccentPrimary`** border (Ω-F4). Focus clears
|
||||
when the user clicks a control surface outside it; **the overlay itself is excluded from
|
||||
stealing or clearing focus**, so editing the thing you selected does not deselect it.
|
||||
- **Five design constraints, each of which breaks something if missed:**
|
||||
1. The focus write must land **before** the `hit.kind` switch in `mouseDownDeck`
|
||||
(`editor_input_deck.cpp:31-37`) and must **not consume the click**, or every knob grab and
|
||||
every toggle commit on a focused deck dies.
|
||||
2. **`DeckHit` carries no group id** (`knob_deck.h:190-195`). Either the shell scans
|
||||
`dl.groups` for containment or `DeckHit` gains one — the latter is the smaller lie.
|
||||
3. **`hitTestDeck` returns a miss for a click on group padding** (`knob_deck.cpp:252-278`),
|
||||
which is exactly the "deck panel background" Daniel wants to hit. Containment against
|
||||
`g.box` is the test, not the hit kind.
|
||||
4. **A deck click SETS focus idempotently; it does not toggle to none.** Otherwise every knob
|
||||
tweak on the focused deck unfocuses it. This retires `nextOverlaySelection`'s
|
||||
re-click-clears branch (`deck_groups.cpp:301`) and replaces the radio-id map with a
|
||||
group-id one.
|
||||
5. **Refuse focus changes while a drag is in flight**, and keep `overlayEnv_` transient view
|
||||
state with **no persistence path** — it is what is drawn, not what is played.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **The Gate deck face is pixel-identical to today**, group boxes and cell rects both, asserted
|
||||
in `test_deck_groups_measured.cpp` — except for FILTER and FILTER ENV, whose widths move by
|
||||
exactly one cell in opposite directions.
|
||||
- Trigger's FILTER ENV and AMP ENV cells measure `kDeckCellW`, centred in their reserves, and
|
||||
each group's box width is **identical in both modes**.
|
||||
- `kFilterModAmt`'s `ParamId` is unchanged; `exposedParams()` returns the same list in the same
|
||||
order; no `param/` file is edited.
|
||||
- Every converted control's commit produces the same audible result as the segment it replaced,
|
||||
including the limiter's latency-restart arm.
|
||||
- `kEditorMinWidth` and `kEditorCeilingWidth` are unchanged, and the 82 px budget is unspent.
|
||||
- Clicking a knob, a button and the bare panel of an envelope deck all focus that overlay; the
|
||||
first two still perform their own action; clicking the overlay does not change focus.
|
||||
- The deck's pure fixtures cover the new composition, the centred run and the focus map — the
|
||||
shell layer is not where any of the three is asserted.
|
||||
|
||||
**Prerequisites.** None; concurrent with T1, T2, T3, T5. **Discharges:** Ω.2, Ω.3, Ω.4, Ω-F3,
|
||||
Ω-F4.
|
||||
|
||||
#### Ω-W1-T5 — `overlay-mapping`
|
||||
|
||||
**Goal.** The loop marks say what the mode means, and every overlay that rides the waveform
|
||||
lands on the frame it names.
|
||||
|
||||
**Spec:** Ω.5, Ω.6 and rulings Ω-F1, Ω-F2; `docs/product/instrument-control-surface.md`
|
||||
§6.3(d) and §6.4 **as amended by Ω-F2**.
|
||||
|
||||
**Surface boundary — owns:** `src/core/instrument/ui/waveform_view.{h,cpp}`,
|
||||
`src/shell/instrument/editor_paint_waveform.cpp` (the mark, caption and overlay regions),
|
||||
`editor_input_waveform.cpp`, `editor_controls.cpp` (`waveMarksFor` / `grabbableMarks` only),
|
||||
`src/core/instrument/ui/spline_edit.h`'s `splineOverlayBox` and `envelope_overlay`, and
|
||||
`tests/test_waveform_view.cpp` + `tests/test_component_geometry.cpp`. **Does not own:** any
|
||||
deck module or `editor_*_deck` TU (T4's), the envelope-cache region of
|
||||
`editor_paint_waveform.cpp` (T3's), or — **without an explicit proposal** —
|
||||
`src/shell/panel/draw_kit.cpp` and `src/core/ui/component_geometry.cpp`.
|
||||
|
||||
**Behavior — Ω.5, the marks.**
|
||||
- **Trigger ⇒ the loop pair and the crossfade mark are HIDDEN entirely.** Not Disabled, not
|
||||
dim: absent. **Gate with loop off ⇒ they are still drawn and still grabbable**, styled **grey
|
||||
disabled** rather than today's dim teal at `kMarkAlphaDisabled`
|
||||
(`editor_paint_waveform.cpp:51-53, 285-305`). The drag-to-set-loop gesture survives, which is
|
||||
the whole reason they stay grabbable.
|
||||
- **The `"LOOP \xe2\x80\x94 GATE ONLY"` caption (`editor_paint_waveform.cpp:254`) is removed
|
||||
unconditionally** — in Trigger there is no span left to centre it in.
|
||||
- **The two sibling captions SURVIVE** (`:255`): `DRAG TO SET LOOP` when the pair is parked and
|
||||
`LOOP OFF` when a span is retained. Their box derives from the marks' span, which still
|
||||
exists in Gate, and they carry the one thing the grey styling cannot — *which* off state you
|
||||
are in, and that dragging will fix it. **This is a [propose]-class call made here rather than
|
||||
left open**; reversing it means deleting information, not styling.
|
||||
- **The seams.** `waveMarksFor`'s `present[]` (`editor_controls.cpp:145`) is the correct place
|
||||
to suppress — it also skips label layout. `grabbableMarks` (`:162`) is the separate
|
||||
grabbability fold and must NOT follow it into hiding for the Gate-off case.
|
||||
- **Warning to the track:** `resolveWaveformClaim`'s smallest-area arbitration and `capAtPoint`'s
|
||||
reverse-order resolve (`editor_input_waveform.cpp:65, 89, 111, 212`) are both sensitive to
|
||||
marks entering and leaving the set — `capAtPoint`'s reverse order exists precisely so a
|
||||
coincident pair stays separable and the crossfade (the one mark with no column) can never be
|
||||
shadowed (`core/instrument/CLAUDE.md`, `waveform_view`). Changing which marks are in the set
|
||||
changes what those two resolve, which is the regression risk in this half of the track.
|
||||
|
||||
**Behavior — Ω.6, the mapping. This is a CLASS defect, not a START-marker instance.**
|
||||
|
||||
Two chains that do not agree, verified this pass:
|
||||
|
||||
| | Marker / overlay chain | Waveform draw chain |
|
||||
|---|---|---|
|
||||
| Area | `waveformOverlayArea` returns the **FULL band, no inset** (`waveform_view.cpp:23-25`) | insets **2 px each side**; `waveformColumnCount` = `width − 4` (`component_geometry.cpp:95-98`), columns start at `box.x + 2` (`draw_kit.cpp:292, 311`) |
|
||||
| Domain | `[0, N]` **closed** (`clampFrame`, `waveform_view.cpp:15-19`) | `[0, N)` **half-open** (`columnMinMax`, `peaks.cpp:69-71`) |
|
||||
| Rounding | **round-half-up** (`frameToX`, `:51-58`) | **truncation** |
|
||||
| Phase | a 2 px marker line centred on an integer (`editor_paint_waveform.cpp:298, :301`) | a 1 px column centred at +0.5 |
|
||||
|
||||
Four independent discrepancies. The 2 px inset dominates — frame 0's marker sits 2 px left of
|
||||
the first column and the error reverses sign toward the end — and the closed domain puts the
|
||||
last frame one pixel outside the band. **Everything riding the overlay inherits all four:** the
|
||||
loop span fill, the crossfade wedges, the staged envelope polyline, and the spline contour —
|
||||
whose `spline_edit.h` guarantee that it uses "the FULL area, no inset, so the drawn contour
|
||||
stays 1:1 with the sample's time axis" **is currently false by 2 px.**
|
||||
|
||||
- **Mandate ONE mapping both chains read**, so the drift cannot recur. Two routes:
|
||||
- **Inset the overlay** (recommended) — instrument-only, and the overlay is already the
|
||||
distinct `OverlayArea` type, so the change has one construction site.
|
||||
- **De-inset the waveform** — `[propose]`, **requires justification before it is taken**: it
|
||||
touches the shared `draw_kit`, which also serves the docked bank panel and the browse-card
|
||||
thumbnails, so its blast radius is three surfaces rather than one.
|
||||
- **The zero-crossing snap must be preserved EXACTLY.** `nearestZeroCrossing`
|
||||
(`waveform_view.cpp:170-199`) fans out symmetrically, probing `t−d` before `t+d` so an
|
||||
equidistant tie resolves to the lower frame; it runs on the mono cache at drag time and is
|
||||
exempt for the crossfade handle. **Behaviour-identical after the change** is an acceptance
|
||||
criterion, not an assumption.
|
||||
- **One question the track must ANSWER, not dodge:** what "sample-accurate" means when
|
||||
`frames < columns` and one frame spans many pixel columns. A marker must land somewhere
|
||||
inside its own frame's column span; which end, and whether the inverse `xToFrame`
|
||||
(`:60-70`) still round-trips, are the track's to specify.
|
||||
- **The two pinned fixtures are re-DERIVED from the new contract, never re-baselined.**
|
||||
`test_waveform_view.cpp` and `test_component_geometry.cpp` pin the current mapping by
|
||||
construction; a test updated to match whatever the code now prints has stopped being a test.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- One mapping function serves both chains; a second one anywhere in the tree fails review.
|
||||
- Every mark, the loop span fill, both crossfade wedges, the staged polyline and the spline
|
||||
contour land on the same pixel as the waveform column for the frame they name — at frame 0,
|
||||
at the last frame, and at an interior frame, asserted in the pure fixtures.
|
||||
- `frames < columns` and `frames > columns` are both covered.
|
||||
- The zero-crossing snap returns the identical frame for the identical input, ties included.
|
||||
- Trigger shows no loop marks and no loop caption; Gate-with-loop-off shows grey marks that
|
||||
still drag, and dragging one still turns the enable on.
|
||||
- `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 agree with the shipped
|
||||
behaviour.
|
||||
|
||||
**Prerequisites.** None; concurrent with T1–T4. **Discharges:** Ω.5, Ω.6, Ω-F1, Ω-F2.
|
||||
|
||||
---
|
||||
|
||||
### Ω-W2 — What the measurements say, and what only a DAW can
|
||||
|
||||
**Depends on:** Ω-W1 in full. **Three tracks.** T1 is **measurement-gated** — it may turn out
|
||||
not to be worth doing, and that is a legitimate outcome. T3's deliverable is a **RECORD, not a
|
||||
fix** (the Λ-W3 precedent).
|
||||
|
||||
| Track | Owns |
|
||||
|---|---|
|
||||
| **T1** `view-topology-cache` | `src/core/view/` — tree / `parentOf` / handle-map memoization |
|
||||
| **T2** `content-detect-throttle` | `src/shell/panel/panel_input.cpp` — the 30 Hz detector's cadence |
|
||||
| **T3** `omega-verification-sweep` | `docs/VERIFICATION.md` — the DAW record for all of Ω-W1 |
|
||||
|
||||
**Shared files in the wave:** none. The three tracks touch three disjoint directories.
|
||||
|
||||
#### Ω-W2-T1 — `view-topology-cache` *(measurement-gated on Ω-W1-T1's result)*
|
||||
|
||||
**Goal.** Stop rebuilding the project's folder topology from scratch on every apply — if, after
|
||||
Ω-W1-T1, that is still a cost worth paying for.
|
||||
|
||||
**Spec:** Ω.7's residual.
|
||||
|
||||
**Surface boundary — owns:** `src/core/view/` (the folder tree, `parentOf`, and the
|
||||
GUID→handle map's shape). **Does not own:** `shell/view/view.cpp` — Ω-W1-T1 owns it, and this
|
||||
track lands after.
|
||||
|
||||
**Behavior.** `readFolderEntries` + `buildFolderTree` run on every apply, and `visibleTracks`
|
||||
walks the tree per parent. **The gate:** Ω-W1-T1 removes the undo-mask cost, the redundant FX
|
||||
writes and the O(T²) resolve; if the remaining apply time is already inside the frame budget on
|
||||
the measured project, **this track is not dispatched** and its finding is recorded instead.
|
||||
`docs/TODO.md` already carries `view_mode_model.cpp` at 815 lines with a named codec seam — do
|
||||
not take that seam here, and do not grow that file.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- A before/after measurement on the same project decides the track, and the number is recorded
|
||||
either way.
|
||||
- If taken: membership, reconcile and the park/restore round trip are all unchanged, and the
|
||||
cache is invalidated by every path that can change track topology.
|
||||
- `core/view` stays pure and CTest-covered.
|
||||
|
||||
**Prerequisites.** Ω-W1-T1 landed and measured. **Discharges:** Ω.7's residual, or records it
|
||||
as not worth taking.
|
||||
|
||||
#### Ω-W2-T2 — `content-detect-throttle`
|
||||
|
||||
**Goal.** The auto-tag detector stops spending main-thread time 30 times a second on exactly
|
||||
the projects where the switch is slowest.
|
||||
|
||||
**Spec:** Ω.7's third cost.
|
||||
|
||||
**Surface boundary — owns:** `src/shell/panel/panel_input.cpp` (`detectNewContent` /
|
||||
`enumerateLiveGuids` and their cadence inside `bankPanelRefresh`). **Does not own:** any other
|
||||
panel TU, `view.cpp`, or the auto-tag RULES — only how often they are asked.
|
||||
|
||||
**Behavior.** `detectNewContent` (`panel_input.cpp:146`) runs `enumerateLiveGuids` (`:106-135`)
|
||||
every tick of REAPER's `"timer"` registration (`main.cpp:341`, ~30/sec), regardless of panel
|
||||
state — O(T + I) REAPER string calls plus ~4(T+I) allocations per tick, on every project. Throttle
|
||||
the ENUMERATION, not the detection semantics: the same GUIDs must still be detected, the
|
||||
first-poll and `reloadPending` re-baseline guards must still fire before any diff, and the
|
||||
lane-minting pass must still run only on a tick that tagged. A slower cadence is a latency
|
||||
change to an invisible background tag, and the track states the chosen interval and why.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- New tracks and items are still auto-tagged into the active mode, and a project reload still
|
||||
re-baselines rather than mass-tagging.
|
||||
- The Ρ-W1-T1 rule survives: an added GUID that already carries a membership record is still
|
||||
dropped, so a render-in-place result is not re-tagged to Design.
|
||||
- The per-tick allocation count is measured before and after.
|
||||
- The panel's own repaint cadence is unaffected.
|
||||
|
||||
**Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost.
|
||||
|
||||
#### Ω-W2-T3 — `omega-verification-sweep`
|
||||
|
||||
**Goal.** The DAW-observable half of this phase is recorded rather than assumed.
|
||||
|
||||
**Spec:** every `[verify — DAW]` criterion in Ω-W1.
|
||||
|
||||
**Surface boundary — owns:** `docs/VERIFICATION.md` (a Phase-Ω section). **Does not own:** any
|
||||
source file. **A fix found here opens a track; it is not applied inside this one.**
|
||||
|
||||
**Behavior.** One session, on a project big enough to reproduce Ω.7 (the track records how big
|
||||
that turned out to be, because that number is itself a finding). Covers: switch-to-first-paint
|
||||
before and after; two switches in rapid succession; the park/restore round trip after
|
||||
deferral; Ctrl-Z after a switch; the meter's frame interval and its behaviour under host load;
|
||||
the Gate deck face against a pre-Ω screenshot; the Trigger face's spacing; every converted
|
||||
button in all three states; deck-click overlay focus; the marks in Trigger and in
|
||||
Gate-with-loop-off; and a marker aligned against the waveform at both ends of a sample.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- Every `[verify — DAW]` mark in Ω-W1 is discharged by a recorded observation or explicitly
|
||||
carried forward as unrun. **Inspection is not an observation.**
|
||||
- The two performance numbers are recorded with the machine and project they were measured on.
|
||||
|
||||
**Prerequisites.** All of Ω-W1 landed. **Discharges:** Ω's DAW criteria.
|
||||
|
||||
### Shared files across Ω's waves, named rather than discovered at merge
|
||||
|
||||
All textual adjacency unless marked otherwise. Within Ω-W1 the wave's own table above is the
|
||||
authority; this one covers the cross-wave and cross-phase cases.
|
||||
|
||||
| File | Tracks | Nature |
|
||||
|---|---|---|
|
||||
| `src/shell/view/view.cpp` | Ω-W1-T1 alone | **Deliberately not split.** Already 642 lines and over the ceiling with its seam blocked; T1's new code goes in a new TU beside it rather than into it |
|
||||
| `src/core/view/*` | Ω-W2-T1 alone | Different wave from T1's shell edits, and gated on them |
|
||||
| `src/app/CMakeLists.txt` / `src/app/main.cpp` | Ω-W1-T1 | **Cross-phase:** Λ owns other regions of both (Λ-W2-T1/T2/T3, Λ-W4-T3, Λ-W5-T1). Ω lands first; Λ rebases |
|
||||
| `src/shell/panel/panel_input.cpp` | Ω-W2-T2 | **Cross-phase:** Ρ-W1-T1's `detectNewContent` edit is landed and is the same function — read what shipped, not what was specced |
|
||||
| `docs/VERIFICATION.md` | Ω-W2-T3 | Λ-W3-T1 writes its own Linux record into a different section |
|
||||
|
||||
---
|
||||
|
||||
## Phase Λ — ReaSampler on Linux: both artifacts, shipped
|
||||
|
||||
**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented
|
||||
@@ -1224,6 +1891,20 @@ proof it exists to give.
|
||||
dispatch and only Λ-F4 gates a wave; see "Decision state" above. It is also the one phase
|
||||
whose acceptance criteria cannot be checked from the current box at all, which is why
|
||||
Λ-W3 exists as a wave rather than as a checklist at the end.
|
||||
- **All of Phase Ω** (`pom-*`). **Eight tracks across two waves** (W1 five, W2 three), from a
|
||||
direct list of seven defects and refinements (Daniel, 2026-08-03), not from `TODO-1.0.md`.
|
||||
Listed here as a block, like Γ, Ψ, Ε, Ρ and Λ; **like Ψ it has no backing product doc** — the
|
||||
seven are recorded in its phase header as Ω.1…Ω.7, and its six rulings are indexed in the
|
||||
table there rather than in `docs/product/`. It **supersedes nothing** but **amends two
|
||||
settled statements**, which is what distinguishes it from the other doc-less phase: Ω-F2
|
||||
reverses the marker "Disabled rather than hidden" principle in
|
||||
`docs/product/instrument-control-surface.md` §6.3(d)/§6.4 for the Trigger case, and Ω-F6
|
||||
amends `src/shell/view/CLAUDE.md`'s accepted FX-parking load hitch. Its six [Daniel]-class
|
||||
forks were opened and ruled the same day it was framed, so no track here is gated on a
|
||||
decision; see "Decision state" above. **Two `docs/TODO.md` entries constrain it without being
|
||||
discharged by it:** `reasampler_editor.h`'s ceiling margin (three W1 tracks add state to it,
|
||||
and the drag-state seam is explicitly out of scope) and `view.cpp`'s blocked split (Ω-W1-T1
|
||||
adds a TU beside it rather than growing it).
|
||||
|
||||
### Deliberate compressions
|
||||
|
||||
@@ -1412,6 +2093,59 @@ Phase Rho — Render in place (none of the seventeen; a direct reque
|
||||
that file are landed and are other functions) — the only pre-existing shell files
|
||||
touched. Disjoint from Gamma (core+shell/instrument) and Epsilon (core+shell/package).
|
||||
|
||||
Phase Omega — Responsiveness + control surface (none of the seventeen; a direct list of seven)
|
||||
W1 The five defects, in parallel [5 tracks, file-disjoint]
|
||||
T1 mode-switch-responsiveness .. Omega.7 structural: undo mask (-1 = UNDO_STATE_ALL,
|
||||
pulls every FX chunk), unconditional SetOffline,
|
||||
PreventUIRefresh (used NOWHERE in src today),
|
||||
compare-before-write, resolve() map, and the
|
||||
F6 split: UI/routing sync, per-FX park DEFERRED.
|
||||
New TU beside view.cpp (642 lines, over ceiling).
|
||||
T2 mode-switch-persist ......... Omega.7 persist: a switch rewrites all SEVEN
|
||||
ext-state keys when only view_state moved.
|
||||
T3 meter-rate .................. Omega.1: ONE 500ms timer today -> second timer id
|
||||
for the meter; 500ms poll body UNCHANGED.
|
||||
Paint: dirty rect, hoisted bitmap, hero-envelope
|
||||
cache. 60 FPS target, 30 the measured fallback.
|
||||
T4 deck-reflow-focus-toggles ... Omega.2 + Omega.3 + Omega.4. Filter Mod -> FILTER
|
||||
ENV last-in-run (id 1240 FROZEN, annotate only);
|
||||
run CENTRED at kDeckCellW; single buttons in two
|
||||
variants (enable = AccentPrimary|dim gray, mode
|
||||
selector = reads current mode, no off state);
|
||||
whole env deck = overlay target, radios gone but
|
||||
DeckRadioDesc survives for MASTER's passive lamp.
|
||||
[F3 + F4 RULED. Gate face stays PIXEL-IDENTICAL.
|
||||
COST: the filter tie-line is unreachable after
|
||||
the move — stated, not discovered]
|
||||
T5 overlay-mapping ............. Omega.5 + Omega.6. Trigger = marks HIDDEN; Gate
|
||||
loop-off = GREY DISABLED and still grabbable;
|
||||
GATE ONLY caption gone, the other two survive.
|
||||
ONE mapping for both chains — four discrepancies
|
||||
(2px inset, round vs truncate, closed vs half-open,
|
||||
half-pixel phase). Overlay-inset route recommended;
|
||||
draw_kit route is [propose]. Zero-crossing snap
|
||||
behaviour-identical. Fixtures RE-DERIVED.
|
||||
[F1 + F2 RULED]
|
||||
W2 Measurement, and what only a DAW can say [3 tracks]
|
||||
T1 view-topology-cache ......... core/view memoization [GATED on W1-T1's measurement;
|
||||
"not worth taking" is a legitimate outcome]
|
||||
T2 content-detect-throttle ..... the 30/sec enumerate, throttled not weakened
|
||||
T3 omega-verification-sweep .... the deliverable is a RECORD, not a fix
|
||||
|
||||
Small tidy work sequenced BEFORE Lambda, and deliberately not another Lambda: no format,
|
||||
no wire version, no parameter id, no new artifact. All six D-rulings SETTLED 2026-08-03
|
||||
(F1 widen the mapping scope; F2 hidden in Trigger / grey-disabled in Gate; F3 single
|
||||
button extends to Filter, Loop, Mono|Stereo; F4 AccentPrimary not AccentHot; F5 both
|
||||
entry surfaces; F6 separate UI from processing). ZERO open forks.
|
||||
Two invariant amendments are track deliverables: instrument-control-surface.md 6.3(d)
|
||||
+ 6.4 (T5) and shell/view/CLAUDE.md's FX-parking load hitch (T1).
|
||||
The solo surface index is EXONERATED (~2T calls, sub-ms) — do not re-litigate it.
|
||||
reasampler_editor.h has ~36 lines of ceiling margin and THREE W1 tracks add state to it;
|
||||
the drag-state seam TODO.md names is OUT OF SCOPE — a track over the ceiling stops and
|
||||
flags. Shared files, named: reasampler_editor.h (T3|T4), editor_paint_waveform.cpp
|
||||
(T3 envelope cache | T5 marks+mapping), editor_controls.cpp (T4 deckDescs | T5
|
||||
waveMarksFor), app/CMakeLists.txt + main.cpp (T1, cross-phase with Lambda).
|
||||
|
||||
Phase Lambda — ReaSampler on Linux (none of the seventeen; a direct request 2026-08-02)
|
||||
W1 The audits [COMPLETE]
|
||||
T1 build-toolchain-audit ....... L-01..L-10, V1..V10, D1..D7
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user