Ω-W1-T3 remediation: meter stays live and washed under the curve popup, not frozen

This commit is contained in:
2026-08-03 14:04:14 -04:00
parent dd067a192e
commit 07b51dcabf
5 changed files with 37 additions and 19 deletions
+8 -4
View File
@@ -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 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 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 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` meter is covered or absent** (Browse, the empty state) `invalidateMeter` skips invalidating
skips invalidating anything at all rather than falling back to a whole-client repaint — the anything at all rather than falling back to a whole-client repaint — the ballistics still
ballistics still advance on `onMeterTimer`'s own clock, but nothing visible changed, so a advance on `onMeterTimer`'s own clock, but nothing visible changed, so a 60 FPS whole-client
60 FPS whole-client repaint under a modal sheet would be pure cost for zero pixels shown. 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 - 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 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 applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and
+6 -4
View File
@@ -80,10 +80,12 @@ void ReaSamplerEditor::paint(HDC hdc, const RECT& dirty) {
Font::Label, Role::BgBase); Font::Label, Role::BgBase);
} }
// A sheet layered over the face covers the meter, so the fast path must not paint // 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 // through it even if something else invalidates a rect that happens to match the stale
// stale field bounds — invalidateMeter itself already skips while covered (see there). // field bounds — invalidateMeter itself already skips while covered (see there). The
if (view_ == View::kBrowse || curvePopup_ != CurveTarget::kNone) meterRects_ = {}; // 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); BitBlt(hdc, dx, dy, dr - dx, db - dy, bmp->getDC(), dx, dy, SRCCOPY);
@@ -7,6 +7,7 @@
#ifdef _WIN32 #ifdef _WIN32
#include <cassert>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -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 // 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. // contract means suppressed — not a zero-height field to fill, tick twelve times and cap.
if (m.field.empty()) return; 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); 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 // 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) { void ReaSamplerEditor::paintMeterField(LICE_IBitmap* bmp) {
drawMeterField(bmp, meterRects_, masterMeter_); 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) { void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
+10 -7
View File
@@ -77,13 +77,16 @@ void ReaSamplerEditor::invalidate() {
void ReaSamplerEditor::invalidateMeter() { void ReaSamplerEditor::invalidateMeter() {
if (!childHwnd_) return; if (!childHwnd_) return;
// The meter is covered (Browse, the curve popup) or has nothing to draw (empty state) — // The meter is covered (Browse) or has nothing to draw (empty state) — ballistics still
// ballistics still advance in onMeterTimer, but nothing on screen changed, so invalidating // advance in onMeterTimer, but nothing on screen changed, so invalidating anything here
// anything here would only buy a whole-client repaint of chrome/waveform/deck the meter // would only buy a whole-client repaint of chrome/waveform/deck the meter never touches.
// never touches. Distinct from "bounds not resolved yet" below: this is a fact about what // Distinct from "bounds not resolved yet" below: this is a fact about what the face is
// the face is showing, not about whether meterRects_ happens to be populated. // showing, not about whether meterRects_ happens to be populated. The curve popup does NOT
const bool meterOnScreen = // cover the meter — its centered sheet tops out at 520x380 (curve_popup.h) against the
view_ == View::kSample && curvePopup_ == CurveTarget::kNone && !selectedId_.empty(); // 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; if (!meterOnScreen) return;
const Rect& f = meterRects_.field; const Rect& f = meterRects_.field;
+3 -4
View File
@@ -335,10 +335,9 @@ private:
// fold, so a drawn envelope disables them through the same predicate. // fold, so a drawn envelope disables them through the same predicate.
bool loopControlsLive() const; bool loopControlsLive() const;
// Which marks the band DRAWS, and which of those accept a grab. They differ in exactly one // Which marks the band DRAWS, and which of those accept a grab. Trigger has no loop at all,
// place — Trigger, where the loop marks stay drawn (hiding a set loop on a mode flip would // so the pair and crossfade are ABSENT rather than drawn inert (an offered-but-refused
// destroy information the user put there) but refuse every gesture, because that refusal // gesture reads as broken) — drawn iff grabbable, so the two cannot currently diverge.
// comes from the engine and no drag can talk it out of it.
instrument::ui::WaveMarks waveMarksFor(const SetupMarkers& m) const; instrument::ui::WaveMarks waveMarksFor(const SetupMarkers& m) const;
instrument::ui::WaveMarks grabbableMarks(const SetupMarkers& m) const; instrument::ui::WaveMarks grabbableMarks(const SetupMarkers& m) const;