From 2befb42d718c672651d4edcc5d0810fee680aa54 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 29 May 2026 12:37:38 -0700 Subject: [PATCH] perf: throttle idle FPS and pause rendering when unfocused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main loop redrew the whole scene at 60 fps unconditionally, keeping the GPU (and, on software GL, the CPU) busy even while idle — enough to heat the machine. Add two cooling measures (desktop only): - Idle FPS throttle: drop to 10 fps when nothing needs animating (no input, mouse movement, playback, loading/background STFT, drag/pan, or counting-down notice), with a 0.5s grace window; snap back to 60 fps on activity. Measured ~34% -> ~8% idle CPU on real hardware. - Focus pause: when the window isn't focused, skip the frame entirely — pump events via PollInputEvents() + WaitTime() with zero drawing, so refocus/close still register. Guarded with !headless so the hidden render window still draws its single capture frame. Also fix two -Wformat-truncation warnings (surfaced at -O2) in the tx_frame label path: widen pos[16]->[28] for the worst-case " %d/%d", and the caller's label buffer lbl[64]->[160] since base can be note[96] (DrawBoxLabel scissor-clips to the box, so behavior is unchanged). Co-Authored-By: Claude Opus 4.8 --- src/render.c | 8 ++++++-- src/spectrogram.c | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/render.c b/src/render.c index 7d5dad1..a60e410 100644 --- a/src/render.c +++ b/src/render.c @@ -965,7 +965,9 @@ static unsigned char AlphaForEvent(int eventIndex, float fillMultiplier) // "BULK 3/4 3/6" or "PRESENCE/SEED". Falls back to the producer note. static void FormatTxFrameLabel(const MlnlEvent* e, char* out, int cap) { - char pos[16] = { 0 }; + // Sized for the worst case " %d/%d" with two full-width ints (2 + 11 + 1 + // + 11 + NUL = 26) so -Wformat-truncation stays quiet at -O2. + char pos[28] = { 0 }; if (e->has_seqn && e->nFrames > 1) snprintf(pos, sizeof(pos), " %d/%d", e->seq + 1, e->nFrames); @@ -1036,7 +1038,9 @@ void DrawAnnotations(Rectangle bounds) Color stroke = (Color){ c.r, c.g, c.b, strokeA }; DrawRectangleRec(r, (Color){ c.r, c.g, c.b, fillA }); DrawRectangleLinesEx(r, 1.5f, stroke); - char lbl[64]; + // Sized to hold the longest base (note[96]) + rate + position without + // truncation; DrawBoxLabel scissor-clips it to the box width anyway. + char lbl[160]; FormatTxFrameLabel(e, lbl, sizeof(lbl)); DrawBoxLabel(r, lbl, stroke, /*topInside=*/true); diff --git a/src/spectrogram.c b/src/spectrogram.c index dd7947d..e034b0c 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -70,6 +70,36 @@ static bool IsUserInteracting(void) return false; } +// Idle FPS throttle. raylib re-renders the whole scene every frame regardless +// of whether anything changed, so an idle window still pins the GPU (and, on a +// software GL stack, the CPU) at 60 fps. We drop to IDLE_FPS whenever nothing +// needs animating, and snap back to ACTIVE_FPS the moment activity resumes. +#define ACTIVE_FPS 60 +#define IDLE_FPS 10 +#define IDLE_GRACE_SECONDS 0.5 // stay at full rate briefly after the last activity + +/** + * Returns true if the frame must keep redrawing at full rate: live input, a + * moving mouse (hover readouts/tooltips), playback, in-progress loading or + * background STFT, an active drag/pan/divider, or a counting-down notice. + * Everything else is a static frame we can throttle. + */ +static bool IsAppActive(void) +{ + if (IsUserInteracting()) return true; + Vector2 d = GetMouseDelta(); + if (d.x != 0.0f || d.y != 0.0f) return true; // hover / cursor readout + if (IsWindowResized()) return true; + if (app.isPlaying) return true; // playhead is moving + if (app.loaded && !app.stftComputed) return true; // STFT still loading + if (app.isBgProcessing && !app.bgFinished) return true;// background high-res fill + if (app.view.isPanning || app.isDividing) return true; + if (app.sel.isDragging || app.sel.isTimeSelecting || app.sel.isFreqSelecting) return true; + if (app.marker.dragging) return true; + if (app.exportMessageTimer > 0.0f) return true; // notification countdown + return false; +} + // Fraction of the view area used by the spectrogram. When the scope is hidden // the spectrogram fills the whole area (divider at the bottom); otherwise the // scope takes the remainder below dividerY. @@ -751,6 +781,28 @@ int main(int argc, char* argv[]) #ifdef __EMSCRIPTEN__ // Track the browser viewport (fill + reflow on resize, like desktop). SyncCanvasToWindow(); +#else + // When the window isn't focused there's nothing for the user to see, so + // skip the entire frame: pump window events (so focus regain and the + // close button still register) and idle, with zero drawing. The + // headless render uses a hidden window that never reports focus, so it + // is exempt — otherwise it would never draw its single capture frame. + if (!headless && !IsWindowFocused()) { + PollInputEvents(); + WaitTime(0.1); // ~10 Hz event poll, no rendering + continue; + } + + // Idle FPS throttle (desktop only; the browser drives RAF/vsync itself). + // Keep full rate while active or within a short grace window after the + // last activity, then fall back to a low idle rate. + { + static double lastActive = -1000.0; + static int curFps = ACTIVE_FPS; + if (IsAppActive()) lastActive = GetTime(); + int wantFps = (GetTime() - lastActive < IDLE_GRACE_SECONDS) ? ACTIVE_FPS : IDLE_FPS; + if (wantFps != curFps) { SetTargetFPS(wantFps); curFps = wantFps; } + } #endif // Drag & Drop