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
+48 -46
View File
@@ -1049,72 +1049,69 @@ int main(int argc, char* argv[])
}
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.view.isPanning = false;
// Foreground high-res: when user zooms in, compute missing
// segments in the visible range immediately (responsive).
// Background task handles the rest when idle.
if (app.skipFactor > 1 && app.stft.numSegments > 0 && !app.bgFinished) {
float viewRange = app.view.end - app.view.start;
// ---- Progressive full-resolution fill ----
// After the strided overview loads, the missing segments are filled
// in at full resolution: the visible range first (foreground, so a
// zoom-in sharpens immediately), then a monotonic sweep of the whole
// 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) {
// Clamp to valid segment range
// Foreground: fill missing segments in the visible range right away.
// Must NOT move the background sweep cursor (app.bgHighResSeg), or the
// sweep would skip everything before the current view and strand it.
if (app.stftComputed && !app.bgFinished && app.stft.numSegments > 0 &&
app.view.end - app.view.start <= 0.25f) {
int viewStartSeg = (int)(app.view.start * app.stft.numSegments);
int viewEndSeg = (int)(app.view.end * app.stft.numSegments);
if (viewStartSeg < 0) viewStartSeg = 0;
if (viewStartSeg >= app.stft.numSegments) viewStartSeg = app.stft.numSegments - 1;
if (viewEndSeg >= app.stft.numSegments) viewEndSeg = app.stft.numSegments - 1;
// Find first missing segment in the visible range and compute it
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) {
int startSeg = seg;
int endSeg = seg + 50;
if (endSeg > viewEndSeg + 1) endSeg = viewEndSeg + 1;
app.bgHighResSeg = ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, startSeg, endSeg);
app.visibleTextureValid = false;
TraceLog(LOG_INFO, "Foreground high-res (%d to %d)", startSeg, endSeg - 1);
ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, seg, endSeg);
fillingDirty = true;
break;
}
}
}
}
// Background high-res: when user is idle, fill in remaining
// segments at full resolution. Pauses on any interaction.
// Also kicks in when zoomed out (no foreground trigger) to fill
// segments outside the view range.
bool isZoomedIn = (app.skipFactor > 1 && app.view.end - app.view.start <= 0.25f);
if (app.isBgProcessing && !app.bgFinished && !IsUserInteracting()) {
// Background: when idle, sweep the whole file from the cursor and
// compute any still-missing segment (already-computed ones are skipped
// cheaply). Done once the cursor passes the last segment.
if (app.stftComputed && !app.bgFinished && app.stft.numSegments > 0 &&
!IsUserInteracting()) {
int endSeg = app.bgHighResSeg + 50; // chunks of 50 segments
if (endSeg > app.stft.numSegments) endSeg = app.stft.numSegments;
app.bgHighResSeg = ComputeNextHighResChunk(&app.signal, &app.stft, app.fftSize, app.bgHighResSeg, endSeg);
fillingDirty = true;
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.isBgProcessing = false;
TraceLog(LOG_INFO, "Background high-res complete (%d segments)", app.stft.numSegments);
// Save the full-res result to cache (overwrites the overview-only entry)
SaveToCache();
TraceLog(LOG_INFO, "Full-res fill complete (%d segments)", app.stft.numSegments);
SaveToCache(); // overwrite the overview-only cache entry
}
}
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
if (!isZoomedIn && app.isBgProcessing && !app.bgFinished && app.bgHighResSeg < app.stft.numSegments) {
bool hasMissing = false;
for (int i = app.bgHighResSeg; i < app.stft.numSegments; i++) {
if (app.stft.segments[i].spectrum == NULL) { hasMissing = true; break; }
}
if (!hasMissing) {
// No more missing segments — mark complete
app.bgFinished = true;
app.isBgProcessing = false;
SaveToCache();
// Stay awake while work remains so idle frames keep the sweep moving
// (IsAppActive() checks this); let the loop sleep once it's done.
app.isBgProcessing = !app.bgFinished;
// Reveal freshly-filled segments. Reassigning the whole file is too
// costly to do every frame, so throttle during the sweep and force a
// final pass at completion. Skipped while interacting to avoid a hitch
// mid-gesture — the next idle frame repaints.
if (fillingDirty && (app.bgFinished || !IsUserInteracting())) {
static double lastFillColorize = 0.0;
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.loadingPhase = 0; // Reset — background processing runs outside this block
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)",
app.stft.numSegments, app.skipFactor);
// Save the overview result to cache (will be overwritten when full-res completes)