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
// overflow/scroll strip), and two full-height toggles that collapse the split.
// Each region reuses the M5 grid render loop (waveform thumbnails / empty state).
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, oversampled
// (kWaveformOversample bins per drawn pixel column) for the FA3 gap-free draw.
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, one bin per
// 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).
// * 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
@@ -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
// space); a slot maps back to its ordinal via selectionForSlot.
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
// FA3 anti-alias: thumbnails are computed OVERSAMPLED — kWaveformOversample bins per
// drawn pixel column — and drawWaveform collapses them per column (peaks::columnMinMax)
// so steep transients render as true full-height spans. computeThumbnail clamps the
// request to the frame count.
// FA3 gap-free: request one bin per drawn pixel column; drawWaveform's
// peaks::columnMinMax exact partition makes every column gap-free — overbinning
// produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps
// the request to the frame count.
const int binWidth = kWaveformOversample *
waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
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;
}
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
+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.
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
+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) {
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;
// Render one filled vertical span per pixel column. peaks::columnMinMax merges
// all bins that project to column `col` under the same partition as
// computeEnvelope, so every pixel column is covered with no gaps regardless of
// the bins-to-pixels ratio — callers oversample (kWaveformOversample bins per
// column) so each span shows true extremes. Same dB display compression
// everywhere (bank_grid, pure).
// all bins that project to column `col` under the exact same partition as
// computeEnvelope used to build the envelope, so every pixel column is covered
// with no gaps regardless of the bins-to-pixels ratio. With one bin per column
// (kWaveformOversample == 1) each span covers the true min/max of exactly the
// frames that fall in that column. Same dB display compression everywhere
// (bank_grid, pure).
for (int col = 0; col < innerW; ++col) {
const MinMax mm = columnMinMax(bins, innerW, 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,
int thumbWidth, InteractionState state);
// The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side
// insets), never negative. Callers size the envelope they compute from this: request
// `kWaveformOversample * waveformColumnCount(box)` bins (clamped to the frame count) and
// drawWaveform collapses them per column — the anti-aliasing lever. Requesting fewer bins
// than columns still renders gap-free (the enclosing bin fills each column) but at the
// envelope's coarser resolution.
int waveformColumnCount(const KitBox& box);
// waveformColumnCount — declared in component_geometry.h (already included above). Returns
// the drawable column count inside `box` (box.width minus the fixed 2px insets each side).
// Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope;
// overbinning (more bins than columns) costs memory and CPU without changing a rendered
// pixel — peaks::columnMinMax's exact partition already makes the draw gap-free.
// The house oversampling factor for waveform envelopes: bins requested per drawn pixel
// column. Each display column then shows the true min/max of ~4 bins (via
// peaks::columnMinMax), so steep transients render as accurate full-height spans instead
// of aliased single-bin dots.
inline constexpr int kWaveformOversample = 4;
// Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing
// call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of
// 1 means they request exactly one bin per column, which is correct. The gap-free render
// comes from peaks::columnMinMax's exact partition, NOT from extra bins.
inline constexpr int kWaveformOversample = 1;
// 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
// 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,
// the browser cards, and the editor hero all render through this. `box` is the draw
// region; `env` is the per-channel min/max envelope from peaks::computeEnvelope, ideally
// oversampled (see waveformColumnCount / kWaveformOversample above). 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.
// bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope
// does, so every pixel column is always covered). The ONE waveform shape in the system:
// the dock-panel thumbnail, the browser cards, and the editor hero all render through
// this. `box` is the draw region; `env` is the per-channel min/max envelope from
// peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count).
// 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);
} // namespace reasampler
+6 -6
View File
@@ -121,9 +121,9 @@ std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::str
return "?";
}
// The bin count a card's thumbnail is computed at: kWaveformOversample bins per drawn
// thumbnail pixel column (FA3 anti-alias) — drawWaveform collapses them per column via
// peaks::columnMinMax. thumbnailFor clamps the request to the decoded frame count.
// The bin count a card's thumbnail is computed at: one bin per drawn pixel column — the
// gap-free render comes from peaks::columnMinMax's exact partition, not from extra bins.
// thumbnailFor clamps the request to the decoded frame count.
int thumbBins(const BrowserLayout& layout) {
return (std::max)(1, kWaveformOversample *
waveformColumnCount(toKitBox(cardThumbnailRect(layout, 0))));
@@ -1011,9 +1011,9 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
const Rect waveArea = bands.hero;
fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest);
if (frames > 0 && waveArea.width() > 0) {
// FA3 anti-alias: request kWaveformOversample bins per drawn pixel column (clamped
// to the frame count) — drawWaveform collapses them per column via
// peaks::columnMinMax into a gap-free true min/max envelope.
// FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this
// multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact
// partition — extra bins produce no visible change. Clamped to frame count below.
const std::int64_t wantBins =
static_cast<std::int64_t>((std::max)(1, waveformColumnCount(toKitBox(waveArea)))) *
kWaveformOversample;