fix(draw_kit): reclaim 4x waveform oversample; relocate waveformColumnCount to component_geometry

kWaveformOversample set to 1 (was 4) — overbinning produced byte-identical pixels because columnMinMax exact partition already makes the draw gap-free. Comments credit columnMinMax, not oversampling.
This commit is contained in:
2026-07-27 19:37:53 -04:00
parent b3c9fad9ba
commit 1e645adcef
7 changed files with 67 additions and 40 deletions
+6 -6
View File
@@ -13,8 +13,8 @@
// LICE-drawn named-banks tab-page region below (one tab per named bank, an // LICE-drawn named-banks tab-page region below (one tab per named bank, an
// overflow/scroll strip), and two full-height toggles that collapse the split. // overflow/scroll strip), and two full-height toggles that collapse the split.
// Each region reuses the M5 grid render loop (waveform thumbnails / empty state). // Each region reuses the M5 grid render loop (waveform thumbnails / empty state).
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, oversampled // * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, one bin per
// (kWaveformOversample bins per drawn pixel column) for the FA3 gap-free draw. // drawn pixel column; drawWaveform's gap-free render comes from peaks::columnMinMax.
// * an in-memory thumbnail cache keyed by (sample id, draw width, bank generation). // * an in-memory thumbnail cache keyed by (sample id, draw width, bank generation).
// * id-keyed bank management (create / rename / delete / evacuate / activate) and // * id-keyed bank management (create / rename / delete / evacuate / activate) and
// sample move/copy — driven from a tab context menu and a drag — against the B1 // sample move/copy — driven from a tab context menu and a drag — against the B1
@@ -1350,10 +1350,10 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
// BankIndex insertion order. Selection/focus are keyed by the occupied-ordinal (selection // BankIndex insertion order. Selection/focus are keyed by the occupied-ordinal (selection
// space); a slot maps back to its ordinal via selectionForSlot. // space); a slot maps back to its ordinal via selectionForSlot.
const RegionDisplay disp = regionDisplay(region, isBanks, reg); const RegionDisplay disp = regionDisplay(region, isBanks, reg);
// FA3 anti-alias: thumbnails are computed OVERSAMPLED — kWaveformOversample bins per // FA3 gap-free: request one bin per drawn pixel column; drawWaveform's
// drawn pixel column — and drawWaveform collapses them per column (peaks::columnMinMax) // peaks::columnMinMax exact partition makes every column gap-free — overbinning
// so steep transients render as true full-height spans. computeThumbnail clamps the // produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps
// request to the frame count. // the request to the frame count.
const int binWidth = kWaveformOversample * const int binWidth = kWaveformOversample *
waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight}); waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
for (const SlotCellRect& r : disp.slotRects) { for (const SlotCellRect& r : disp.slotRects) {
+5
View File
@@ -100,4 +100,9 @@ int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCou
return row; return row;
} }
int waveformColumnCount(const KitBox& box) {
const int w = box.width - 4; // fixed 2px inset each side (matches drawWaveform)
return w > 0 ? w : 0;
}
} // namespace reasampler } // namespace reasampler
+10
View File
@@ -126,4 +126,14 @@ ListRowBox computeListRow(const KitBox& list, int index, int rowHeight);
// a phantom row. Half-open bounds match computeListRow so the hit maps to the drawn row. // a phantom row. Half-open bounds match computeListRow so the hit maps to the drawn row.
int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCount); int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCount);
// --- Waveform column count ---------------------------------------------------
//
// The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side
// insets), never negative. Callers pass this count directly as the `binCount` argument to
// peaks::computeEnvelope — one bin per column is the correct resolution, and
// peaks::columnMinMax's exact partition makes the render gap-free at any bins-to-pixels
// ratio. Overbinning does NOT improve render quality (columnMinMax's frame union is
// identical whether bins == columns or bins == k*columns) and wastes memory and CPU.
int waveformColumnCount(const KitBox& box);
} // namespace reasampler } // namespace reasampler
+6 -10
View File
@@ -277,11 +277,6 @@ void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
} }
} }
int waveformColumnCount(const KitBox& box) {
const int w = box.width - 4; // fixed 2px inset each side (matches drawWaveform below)
return w > 0 ? w : 0;
}
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) { void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
if (!bmp || box.empty()) return; if (!bmp || box.empty()) return;
@@ -311,11 +306,12 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
if (bins.empty() || innerW <= 0) continue; if (bins.empty() || innerW <= 0) continue;
// Render one filled vertical span per pixel column. peaks::columnMinMax merges // Render one filled vertical span per pixel column. peaks::columnMinMax merges
// all bins that project to column `col` under the same partition as // all bins that project to column `col` under the exact same partition as
// computeEnvelope, so every pixel column is covered with no gaps regardless of // computeEnvelope used to build the envelope, so every pixel column is covered
// the bins-to-pixels ratio — callers oversample (kWaveformOversample bins per // with no gaps regardless of the bins-to-pixels ratio. With one bin per column
// column) so each span shows true extremes. Same dB display compression // (kWaveformOversample == 1) each span covers the true min/max of exactly the
// everywhere (bank_grid, pure). // frames that fall in that column. Same dB display compression everywhere
// (bank_grid, pure).
for (int col = 0; col < innerW; ++col) { for (int col = 0; col < innerW; ++col) {
const MinMax mm = columnMinMax(bins, innerW, col); const MinMax mm = columnMinMax(bins, innerW, col);
const int x = box.x + 2 + col; const int x = box.x + 2 + col;
+17 -18
View File
@@ -115,29 +115,28 @@ void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState
void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label, void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
int thumbWidth, InteractionState state); int thumbWidth, InteractionState state);
// The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side // waveformColumnCount — declared in component_geometry.h (already included above). Returns
// insets), never negative. Callers size the envelope they compute from this: request // the drawable column count inside `box` (box.width minus the fixed 2px insets each side).
// `kWaveformOversample * waveformColumnCount(box)` bins (clamped to the frame count) and // Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope;
// drawWaveform collapses them per column — the anti-aliasing lever. Requesting fewer bins // overbinning (more bins than columns) costs memory and CPU without changing a rendered
// than columns still renders gap-free (the enclosing bin fills each column) but at the // pixel — peaks::columnMinMax's exact partition already makes the draw gap-free.
// envelope's coarser resolution.
int waveformColumnCount(const KitBox& box);
// The house oversampling factor for waveform envelopes: bins requested per drawn pixel // Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing
// column. Each display column then shows the true min/max of ~4 bins (via // call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of
// peaks::columnMinMax), so steep transients render as accurate full-height spans instead // 1 means they request exactly one bin per column, which is correct. The gap-free render
// of aliased single-bin dots. // comes from peaks::columnMinMax's exact partition, NOT from extra bins.
inline constexpr int kWaveformOversample = 4; inline constexpr int kWaveformOversample = 1;
// A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per // A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per
// channel and one accent vertical span PER PIXEL COLUMN, each column covering the true // channel and one accent vertical span PER PIXEL COLUMN, each column covering the true
// extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any // extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any
// bins-to-pixels ratio). The ONE waveform shape in the system: the dock-panel thumbnail, // bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope
// the browser cards, and the editor hero all render through this. `box` is the draw // does, so every pixel column is always covered). The ONE waveform shape in the system:
// region; `env` is the per-channel min/max envelope from peaks::computeEnvelope, ideally // the dock-panel thumbnail, the browser cards, and the editor hero all render through
// oversampled (see waveformColumnCount / kWaveformOversample above). An empty env draws // this. `box` is the draw region; `env` is the per-channel min/max envelope from
// just the midline. The caller fills the surface first (or passes a box already filled); // peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count).
// this draws only the wave + midline. // An empty env draws just the midline. The caller fills the surface first (or passes a
// box already filled); this draws only the wave + midline.
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env); void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env);
} // namespace reasampler } // namespace reasampler
+6 -6
View File
@@ -121,9 +121,9 @@ std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::str
return "?"; return "?";
} }
// The bin count a card's thumbnail is computed at: kWaveformOversample bins per drawn // The bin count a card's thumbnail is computed at: one bin per drawn pixel column — the
// thumbnail pixel column (FA3 anti-alias) — drawWaveform collapses them per column via // gap-free render comes from peaks::columnMinMax's exact partition, not from extra bins.
// peaks::columnMinMax. thumbnailFor clamps the request to the decoded frame count. // thumbnailFor clamps the request to the decoded frame count.
int thumbBins(const BrowserLayout& layout) { int thumbBins(const BrowserLayout& layout) {
return (std::max)(1, kWaveformOversample * return (std::max)(1, kWaveformOversample *
waveformColumnCount(toKitBox(cardThumbnailRect(layout, 0)))); waveformColumnCount(toKitBox(cardThumbnailRect(layout, 0))));
@@ -1011,9 +1011,9 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
const Rect waveArea = bands.hero; const Rect waveArea = bands.hero;
fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest); fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest);
if (frames > 0 && waveArea.width() > 0) { if (frames > 0 && waveArea.width() > 0) {
// FA3 anti-alias: request kWaveformOversample bins per drawn pixel column (clamped // FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this
// to the frame count) — drawWaveform collapses them per column via // multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact
// peaks::columnMinMax into a gap-free true min/max envelope. // partition — extra bins produce no visible change. Clamped to frame count below.
const std::int64_t wantBins = const std::int64_t wantBins =
static_cast<std::int64_t>((std::max)(1, waveformColumnCount(toKitBox(waveArea)))) * static_cast<std::int64_t>((std::max)(1, waveformColumnCount(toKitBox(waveArea)))) *
kWaveformOversample; kWaveformOversample;
+17
View File
@@ -173,6 +173,22 @@ static void testListRowLayoutHitAgreement() {
} }
} }
// --- waveformColumnCount -----------------------------------------------------
static void testWaveformColumnCount() {
// Normal box: width minus the two 2px side insets.
CHECK(waveformColumnCount(KitBox{0, 0, 100, 40}) == 96);
CHECK(waveformColumnCount(KitBox{10, 5, 50, 20}) == 46);
// Minimum: a 5-wide box yields exactly 1 drawable column.
CHECK(waveformColumnCount(KitBox{0, 0, 5, 10}) == 1);
// Too narrow to have any drawable columns: 4 or fewer columns returns 0.
CHECK(waveformColumnCount(KitBox{0, 0, 4, 10}) == 0);
CHECK(waveformColumnCount(KitBox{0, 0, 1, 10}) == 0);
// Degenerate (empty) box returns 0 — graceful suppression.
CHECK(waveformColumnCount(KitBox{}) == 0);
CHECK(waveformColumnCount(KitBox{0, 0, 0, 40}) == 0);
}
int main() { int main() {
testHitTestBoxHalfOpen(); testHitTestBoxHalfOpen();
testButtonBoxInset(); testButtonBoxInset();
@@ -190,6 +206,7 @@ int main() {
testListRowHitTest(); testListRowHitTest();
testListRowHitTestBoundedByCount(); testListRowHitTestBoundedByCount();
testListRowLayoutHitAgreement(); testListRowLayoutHitAgreement();
testWaveformColumnCount();
if (g_fail == 0) std::printf("component_geometry: all tests passed\n"); if (g_fail == 0) std::printf("component_geometry: all tests passed\n");
else std::printf("component_geometry: %d CHECK(s) FAILED\n", g_fail); else std::printf("component_geometry: %d CHECK(s) FAILED\n", g_fail);