fix: progressive full-res STFT fill never resumed after interaction

The overview-then-idle high-res fill had two defects that left permanent
black/low-res vertical stripes:

- On any interaction the driver set isBgProcessing=false to pause, but nothing
  ever re-armed it, so the background sweep died on the first pan/zoom and the
  strided overview was never completed. Now isBgProcessing simply tracks
  !bgFinished and the per-frame compute is gated on !IsUserInteracting(), so the
  sweep resumes on its own once the user stops interacting.
- The foreground (visible-range) fill overwrote the background sweep cursor
  bgHighResSeg with the current view position, so the sweep skipped everything
  before the view and stranded it uncomputed. The foreground pass no longer
  touches the cursor; the sweep stays monotonic from 0.

Also recolor the displayed image as segments fill in (throttled during the
sweep, forced at completion) instead of only at the very end, so freshly
computed regions actually appear; and short files whose overview is already
full-resolution (skipFactor 1) skip the sweep entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 16:25:43 -07:00
parent b72e1cef18
commit 83277e5086
+57 -55
View File
@@ -1049,72 +1049,69 @@ int main(int argc, char* argv[])
} }
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.view.isPanning = false; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.view.isPanning = false;
// Foreground high-res: when user zooms in, compute missing // ---- Progressive full-resolution fill ----
// segments in the visible range immediately (responsive). // After the strided overview loads, the missing segments are filled
// Background task handles the rest when idle. // in at full resolution: the visible range first (foreground, so a
if (app.skipFactor > 1 && app.stft.numSegments > 0 && !app.bgFinished) { // zoom-in sharpens immediately), then a monotonic sweep of the whole
float viewRange = app.view.end - app.view.start; // file whenever the user is idle. The sweep RESUMES on its own after
// an interaction — we only gate the per-frame compute, we never latch
// the task off. (Previously an interaction set isBgProcessing=false
// permanently, so the fill died on the first pan/zoom and left
// black/low-res stripes wherever the foreground pass hadn't reached.)
bool fillingDirty = false;
if (viewRange <= 0.25f) { // Foreground: fill missing segments in the visible range right away.
// Clamp to valid segment range // Must NOT move the background sweep cursor (app.bgHighResSeg), or the
int viewStartSeg = (int)(app.view.start * app.stft.numSegments); // sweep would skip everything before the current view and strand it.
int viewEndSeg = (int)(app.view.end * app.stft.numSegments); if (app.stftComputed && !app.bgFinished && app.stft.numSegments > 0 &&
if (viewStartSeg < 0) viewStartSeg = 0; app.view.end - app.view.start <= 0.25f) {
if (viewStartSeg >= app.stft.numSegments) viewStartSeg = app.stft.numSegments - 1; int viewStartSeg = (int)(app.view.start * app.stft.numSegments);
if (viewEndSeg >= app.stft.numSegments) viewEndSeg = app.stft.numSegments - 1; int viewEndSeg = (int)(app.view.end * app.stft.numSegments);
if (viewStartSeg < 0) viewStartSeg = 0;
// Find first missing segment in the visible range and compute it if (viewEndSeg >= app.stft.numSegments) viewEndSeg = app.stft.numSegments - 1;
for (int seg = viewStartSeg; seg <= viewEndSeg && seg < app.stft.numSegments; seg++) { for (int seg = viewStartSeg; seg <= viewEndSeg; seg++) {
if (app.stft.segments[seg].spectrum == NULL) { if (app.stft.segments[seg].spectrum == NULL) {
int startSeg = seg; int endSeg = seg + 50;
int endSeg = seg + 50; if (endSeg > viewEndSeg + 1) endSeg = viewEndSeg + 1;
if (endSeg > viewEndSeg + 1) endSeg = viewEndSeg + 1; ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, seg, endSeg);
app.bgHighResSeg = ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, startSeg, endSeg); fillingDirty = true;
app.visibleTextureValid = false; break;
TraceLog(LOG_INFO, "Foreground high-res (%d to %d)", startSeg, endSeg - 1);
break;
}
} }
} }
} }
// Background high-res: when user is idle, fill in remaining // Background: when idle, sweep the whole file from the cursor and
// segments at full resolution. Pauses on any interaction. // compute any still-missing segment (already-computed ones are skipped
// Also kicks in when zoomed out (no foreground trigger) to fill // cheaply). Done once the cursor passes the last segment.
// segments outside the view range. if (app.stftComputed && !app.bgFinished && app.stft.numSegments > 0 &&
bool isZoomedIn = (app.skipFactor > 1 && app.view.end - app.view.start <= 0.25f); !IsUserInteracting()) {
if (app.isBgProcessing && !app.bgFinished && !IsUserInteracting()) { int endSeg = app.bgHighResSeg + 50; // chunks of 50 segments
int endSeg = app.bgHighResSeg + 50; // chunks of 50 segments
if (endSeg > app.stft.numSegments) endSeg = app.stft.numSegments; if (endSeg > app.stft.numSegments) endSeg = app.stft.numSegments;
app.bgHighResSeg = ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, app.bgHighResSeg, endSeg); app.bgHighResSeg = ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, app.bgHighResSeg, endSeg);
fillingDirty = true;
if (app.bgHighResSeg >= app.stft.numSegments) { if (app.bgHighResSeg >= app.stft.numSegments) {
// All done — generate full-res texture and mark complete
AutoScaleAmplitude(&app.stft);
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.visibleTextureValid = false;
app.bgFinished = true; app.bgFinished = true;
app.isBgProcessing = false; TraceLog(LOG_INFO, "Full-res fill complete (%d segments)", app.stft.numSegments);
TraceLog(LOG_INFO, "Background high-res complete (%d segments)", app.stft.numSegments); SaveToCache(); // overwrite the overview-only cache entry
// Save the full-res result to cache (overwrites the overview-only entry)
SaveToCache();
} }
} }
if (app.isBgProcessing && IsUserInteracting()) {
// Pause background processing — user is interacting
app.isBgProcessing = false;
}
// If not zoomed in, scan for missing segments to kick off processing // Stay awake while work remains so idle frames keep the sweep moving
if (!isZoomedIn && app.isBgProcessing && !app.bgFinished && app.bgHighResSeg < app.stft.numSegments) { // (IsAppActive() checks this); let the loop sleep once it's done.
bool hasMissing = false; app.isBgProcessing = !app.bgFinished;
for (int i = app.bgHighResSeg; i < app.stft.numSegments; i++) {
if (app.stft.segments[i].spectrum == NULL) { hasMissing = true; break; } // Reveal freshly-filled segments. Reassigning the whole file is too
} // costly to do every frame, so throttle during the sweep and force a
if (!hasMissing) { // final pass at completion. Skipped while interacting to avoid a hitch
// No more missing segments — mark complete // mid-gesture — the next idle frame repaints.
app.bgFinished = true; if (fillingDirty && (app.bgFinished || !IsUserInteracting())) {
app.isBgProcessing = false; static double lastFillColorize = 0.0;
SaveToCache(); double now = GetTime();
if (app.bgFinished || now - lastFillColorize > 0.5) {
if (app.bgFinished) AutoScaleAmplitude(&app.stft);
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.visibleTextureValid = false;
lastFillColorize = now;
} }
} }
@@ -1421,7 +1418,12 @@ int main(int argc, char* argv[])
app.stftComputed = true; app.stftComputed = true;
app.loadingPhase = 0; // Reset — background processing runs outside this block app.loadingPhase = 0; // Reset — background processing runs outside this block
app.loadingProgress = 0.0f; app.loadingProgress = 0.0f;
app.isBgProcessing = true; // Kick off background high-res next frame // Arm the progressive full-res fill from the start of the file.
// A full-resolution overview (skipFactor 1) has nothing missing,
// so mark it finished and skip the sweep entirely.
app.bgHighResSeg = 0;
app.bgFinished = (app.skipFactor <= 1);
app.isBgProcessing = !app.bgFinished;
TraceLog(LOG_INFO, "STFT overview computed (%d segments, skipFactor=%d)", TraceLog(LOG_INFO, "STFT overview computed (%d segments, skipFactor=%d)",
app.stft.numSegments, app.skipFactor); app.stft.numSegments, app.skipFactor);
// Save the overview result to cache (will be overwritten when full-res completes) // Save the overview result to cache (will be overwritten when full-res completes)