From 07b51dcabfb2eaee613fb981e85962fd4b651721 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 3 Aug 2026 14:04:14 -0400 Subject: [PATCH] =?UTF-8?q?=CE=A9-W1-T3=20remediation:=20meter=20stays=20l?= =?UTF-8?q?ive=20and=20washed=20under=20the=20curve=20popup,=20not=20froze?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shell/instrument/CLAUDE.md | 12 ++++++++---- src/shell/instrument/editor_paint.cpp | 10 ++++++---- src/shell/instrument/editor_paint_deck.cpp | 10 ++++++++++ src/shell/instrument/editor_platform.cpp | 17 ++++++++++------- src/shell/instrument/reasampler_editor.h | 7 +++---- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/shell/instrument/CLAUDE.md b/src/shell/instrument/CLAUDE.md index b45b9ec..68fb23c 100644 --- a/src/shell/instrument/CLAUDE.md +++ b/src/shell/instrument/CLAUDE.md @@ -285,10 +285,14 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT field stays on the full path: the static numeral gutter (AA text re-blended onto itself every frame thickens) and the GR lamp (drawn straight onto the deck group's gradient, which a sub-rect fill cannot reproduce), so a lamp transition takes a whole-client repaint. **When the - meter is covered or absent** (Browse, the curve popup, the empty state) `invalidateMeter` - skips invalidating anything at all rather than falling back to a whole-client repaint — the - ballistics still advance on `onMeterTimer`'s own clock, but nothing visible changed, so a - 60 FPS whole-client repaint under a modal sheet would be pure cost for zero pixels shown. + meter is covered or absent** (Browse, the empty state) `invalidateMeter` skips invalidating + anything at all rather than falling back to a whole-client repaint — the ballistics still + advance on `onMeterTimer`'s own clock, but nothing visible changed, so a 60 FPS whole-client + repaint under a modal sheet would be pure cost for zero pixels shown. **The curve popup does + NOT cover the meter** — its centered sheet clamps to 520x380 (`curve_popup.h`) against the + meter's fixed right-anchored slot, at any resizable window size — so the fast path stays live + under it; `paintMeterField` reapplies the popup's own 0.50-alpha wash to the field alone so a + meter-only frame doesn't flash through it at full brightness. - The bake's availability probe runs on the SAME tick that paints the button, so the control can never be enabled on one tick and refuse on the next. The bake Hold control's applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and diff --git a/src/shell/instrument/editor_paint.cpp b/src/shell/instrument/editor_paint.cpp index 5b5b5a4..545310b 100644 --- a/src/shell/instrument/editor_paint.cpp +++ b/src/shell/instrument/editor_paint.cpp @@ -80,10 +80,12 @@ void ReaSamplerEditor::paint(HDC hdc, const RECT& dirty) { Font::Label, Role::BgBase); } - // A sheet layered over the face covers the meter, so the fast path must not paint - // through it even if something else invalidates a rect that happens to match the - // stale field bounds — invalidateMeter itself already skips while covered (see there). - if (view_ == View::kBrowse || curvePopup_ != CurveTarget::kNone) meterRects_ = {}; + // Browse is a full-window modal that covers the meter, so the fast path must not paint + // through it even if something else invalidates a rect that happens to match the stale + // field bounds — invalidateMeter itself already skips while covered (see there). The + // curve popup's sheet never reaches the meter column (see invalidateMeter), so its + // meterRects_ stays valid and the fast path stays live under the popup's wash. + if (view_ == View::kBrowse) meterRects_ = {}; } BitBlt(hdc, dx, dy, dr - dx, db - dy, bmp->getDC(), dx, dy, SRCCOPY); diff --git a/src/shell/instrument/editor_paint_deck.cpp b/src/shell/instrument/editor_paint_deck.cpp index 5282110..d09134b 100644 --- a/src/shell/instrument/editor_paint_deck.cpp +++ b/src/shell/instrument/editor_paint_deck.cpp @@ -7,6 +7,7 @@ #ifdef _WIN32 +#include #include #include @@ -62,6 +63,7 @@ void drawMeterField(LICE_IBitmap* bmp, const MeterRects& m, const MasterMeterUi& // A column narrower than the interior needs yields all-empty rects, which under rect.h's // contract means suppressed — not a zero-height field to fill, tick twelve times and cap. if (m.field.empty()) return; + assert(meterBarsWithinField(m)); // the self-containment invariant this draw rests on fillSurface(bmp, toKitBox(m.field), Role::BgCell, InteractionState::Rest); // Scale: a rule every 6 dB, numeralled every 12 with 0 dB heavier — the reference the @@ -111,6 +113,14 @@ void drawMeterField(LICE_IBitmap* bmp, const MeterRects& m, const MasterMeterUi& void ReaSamplerEditor::paintMeterField(LICE_IBitmap* bmp) { drawMeterField(bmp, meterRects_, masterMeter_); + // A full paint under the curve popup washes the whole client at 0.50 alpha AFTER the deck + // draws (paintCurvePopup); a meter-only fast-path frame draws fresh opaque bars into that + // same back buffer, so it must reapply the same wash to the field alone or the meter would + // flash through at full brightness against the dimmed rest of the face. + if (curvePopup_ != CurveTarget::kNone && !meterRects_.field.empty()) { + const Rect& f = meterRects_.field; + LICE_FillRect(bmp, f.x, f.y, f.width, f.height, toLice(roleColor(Role::BgBase)), 0.50f, 0); + } } void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) { diff --git a/src/shell/instrument/editor_platform.cpp b/src/shell/instrument/editor_platform.cpp index 080d256..d329fe5 100644 --- a/src/shell/instrument/editor_platform.cpp +++ b/src/shell/instrument/editor_platform.cpp @@ -77,13 +77,16 @@ void ReaSamplerEditor::invalidate() { void ReaSamplerEditor::invalidateMeter() { if (!childHwnd_) return; - // The meter is covered (Browse, the curve popup) or has nothing to draw (empty state) — - // ballistics still advance in onMeterTimer, but nothing on screen changed, so invalidating - // anything here would only buy a whole-client repaint of chrome/waveform/deck the meter - // never touches. Distinct from "bounds not resolved yet" below: this is a fact about what - // the face is showing, not about whether meterRects_ happens to be populated. - const bool meterOnScreen = - view_ == View::kSample && curvePopup_ == CurveTarget::kNone && !selectedId_.empty(); + // The meter is covered (Browse) or has nothing to draw (empty state) — ballistics still + // advance in onMeterTimer, but nothing on screen changed, so invalidating anything here + // would only buy a whole-client repaint of chrome/waveform/deck the meter never touches. + // Distinct from "bounds not resolved yet" below: this is a fact about what the face is + // showing, not about whether meterRects_ happens to be populated. The curve popup does NOT + // cover the meter — its centered sheet tops out at 520x380 (curve_popup.h) against the + // meter's fixed right-anchored slot, at any resizable size — so the meter stays on screen + // (dimmed by the popup's wash, which paintMeterField reapplies to the field alone) and keeps + // its own rate rather than freezing under the sheet. + const bool meterOnScreen = view_ == View::kSample && !selectedId_.empty(); if (!meterOnScreen) return; const Rect& f = meterRects_.field; diff --git a/src/shell/instrument/reasampler_editor.h b/src/shell/instrument/reasampler_editor.h index ffc843b..8750798 100644 --- a/src/shell/instrument/reasampler_editor.h +++ b/src/shell/instrument/reasampler_editor.h @@ -335,10 +335,9 @@ private: // fold, so a drawn envelope disables them through the same predicate. bool loopControlsLive() const; - // Which marks the band DRAWS, and which of those accept a grab. They differ in exactly one - // place — Trigger, where the loop marks stay drawn (hiding a set loop on a mode flip would - // destroy information the user put there) but refuse every gesture, because that refusal - // comes from the engine and no drag can talk it out of it. + // Which marks the band DRAWS, and which of those accept a grab. Trigger has no loop at all, + // so the pair and crossfade are ABSENT rather than drawn inert (an offered-but-refused + // gesture reads as broken) — drawn iff grabbable, so the two cannot currently diverge. instrument::ui::WaveMarks waveMarksFor(const SetupMarkers& m) const; instrument::ui::WaveMarks grabbableMarks(const SetupMarkers& m) const;