perf: event-driven idle + lazy audio device for near-zero idle CPU
raylib's frame limiter partial-busy-waits ~5% of every frame interval, so capping the FPS couldn't get idle CPU below ~5% of a core, and the audio output device's mixing thread spun continuously even when nothing played. - Idle render: go fully event-driven when idle. While active (focused + something animating, or within a 0.5s grace window) run at ACTIVE_FPS; otherwise EnableEventWaiting() so EndDrawing's PollInputEvents blocks on glfwWaitEvents until the next input/window event. Unfocused windows skip drawing entirely and block. Headless render is exempt (its hidden window gets no events). Replaces the earlier fixed-FPS idle throttle. - ACTIVE_FPS lowered 60 -> 30: ample for a non-game UI, halves active cost. - Lazy audio device: open it on demand at first playback (EnsureAudioDevice) and release it once idle and not playing (ReleaseAudioDevice), reopened on the next play. Never closes mid-playback (gated on !isPlaying), so audio always finishes first. Shutdown is guarded with IsAudioDeviceReady(). Idle/backgrounded window now drops to ~0% CPU (no render busy-wait, no audio thread); snaps back instantly on input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
@@ -216,12 +216,28 @@ static float* BuildSelectionAudio(int* outNumSamples)
|
||||
return regionSamples;
|
||||
}
|
||||
|
||||
void EnsureAudioDevice(void)
|
||||
{
|
||||
if (!IsAudioDeviceReady()) InitAudioDevice();
|
||||
}
|
||||
|
||||
void ReleaseAudioDevice(void)
|
||||
{
|
||||
if (!IsAudioDeviceReady()) return;
|
||||
if (AudioPlaybackSound.frameCount != 0) {
|
||||
UnloadSound(AudioPlaybackSound);
|
||||
AudioPlaybackSound = (Sound){ 0 };
|
||||
}
|
||||
CloseAudioDevice();
|
||||
}
|
||||
|
||||
void PlaySelectedRegion(void)
|
||||
{
|
||||
int numSamples = 0;
|
||||
float* regionSamples = BuildSelectionAudio(&numSamples);
|
||||
if (!regionSamples) return;
|
||||
|
||||
EnsureAudioDevice(); // opened on demand; released again once playback ends
|
||||
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
||||
|
||||
Wave wave = { .data = regionSamples, .frameCount = (unsigned int)numSamples,
|
||||
|
||||
@@ -10,8 +10,16 @@ bool LoadWavFile(const char* filepath, AudioSignal* signal);
|
||||
void FreeSignal(AudioSignal* signal);
|
||||
|
||||
// Play the current time/frequency selection (applies an FFT bandpass filter).
|
||||
// Opens the audio output device on demand (see EnsureAudioDevice).
|
||||
void PlaySelectedRegion(void);
|
||||
|
||||
// Audio output device lifecycle. The device is opened lazily (only when
|
||||
// something actually plays) and released while idle, so a backgrounded window
|
||||
// isn't holding the output device open — keeping it open costs steady CPU in
|
||||
// miniaudio's mixing thread.
|
||||
void EnsureAudioDevice(void); // InitAudioDevice() if not already initialized
|
||||
void ReleaseAudioDevice(void); // unload the playback sound + CloseAudioDevice()
|
||||
|
||||
// Save the current selection (band-limited, time-cropped — same processing as
|
||||
// playback) as a mono WAV in dirPath. Reports the result via app.exportMessage.
|
||||
void ExportSelectionWAV(const char* dirPath);
|
||||
|
||||
+45
-29
@@ -70,12 +70,11 @@ 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
|
||||
// Idle power management. raylib re-renders the whole scene every frame, so an
|
||||
// idle window otherwise pins the GPU (and, on software GL, the CPU) at the
|
||||
// target rate. We run at ACTIVE_FPS while something needs animating, then go
|
||||
// fully event-driven (block until input) when idle — see the loop below.
|
||||
#define ACTIVE_FPS 30 // plenty for a non-game UI; halves active-frame cost
|
||||
#define IDLE_GRACE_SECONDS 0.5 // stay at full rate briefly after the last activity
|
||||
|
||||
/**
|
||||
@@ -621,9 +620,10 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
#endif
|
||||
InitWindow(headless ? reqW : 1280, headless ? reqH : 800, "Spectrogram Viewer");
|
||||
SetTargetFPS(60);
|
||||
SetTargetFPS(ACTIVE_FPS);
|
||||
SetTraceLogLevel(LOG_WARNING); // Suppress INFO texture logs
|
||||
InitAudioDevice();
|
||||
// Audio device is opened lazily on first playback (see EnsureAudioDevice)
|
||||
// and released while idle, so an idle/backgrounded window holds no device.
|
||||
SetExitKey(KEY_NULL); // ESC should not close the window
|
||||
|
||||
// Save original working directory so command-line args resolve correctly
|
||||
@@ -782,26 +782,42 @@ int main(int argc, char* argv[])
|
||||
// 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.
|
||||
{
|
||||
// Desktop power management. raylib's frame limiter partial-busy-waits
|
||||
// ~5% of every frame interval, so capping the FPS can't get idle CPU
|
||||
// below ~5% of a core. Instead we go fully event-driven when idle:
|
||||
// block in PollInputEvents() (glfwWaitEvents) until an input/window
|
||||
// event arrives, so an idle/backgrounded window costs ~0% CPU and only
|
||||
// redraws on demand. Headless render (hidden window, single frame) opts
|
||||
// out — its window never receives events, so waiting would deadlock.
|
||||
if (!headless) {
|
||||
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; }
|
||||
static int waiting = -1; // -1 unset, 0 = active (poll), 1 = idle (event-wait)
|
||||
|
||||
bool focused = IsWindowFocused();
|
||||
if (focused && IsAppActive()) lastActive = GetTime();
|
||||
// Active = focused AND something needs animating (or just did, within
|
||||
// the grace window). Anything else is a static frame we can sleep on.
|
||||
bool active = focused && (GetTime() - lastActive < IDLE_GRACE_SECONDS);
|
||||
|
||||
if (active) {
|
||||
if (waiting != 0) { DisableEventWaiting(); SetTargetFPS(ACTIVE_FPS); waiting = 0; }
|
||||
} else {
|
||||
// Idle: no busy-wait limiter; EndDrawing's PollInputEvents blocks.
|
||||
if (waiting != 1) { SetTargetFPS(0); EnableEventWaiting(); waiting = 1; }
|
||||
// Release the output device while idle — but never mid-playback
|
||||
// (isPlaying gates it, so audio always finishes first). Reopened
|
||||
// on the next play. Safe on the unfocused path too: only closes
|
||||
// when nothing is playing.
|
||||
if (!app.isPlaying && IsAudioDeviceReady()) ReleaseAudioDevice();
|
||||
if (!focused) {
|
||||
// Unfocused: nothing to show. Block on events (refocus/close)
|
||||
// without drawing at all.
|
||||
PollInputEvents();
|
||||
continue;
|
||||
}
|
||||
// Focused but idle: fall through to draw this one static frame,
|
||||
// then EndDrawing blocks until the next event.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1682,7 +1698,7 @@ int main(int argc, char* argv[])
|
||||
|
||||
TraceLog(LOG_INFO, "Shutting down...");
|
||||
if (mainFont.texture.id != 0) UnloadFont(mainFont);
|
||||
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
||||
if (IsAudioDeviceReady() && AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
||||
if (app.stftComputed) { FreeSTFT(&app.stft); UnloadImage(app.spectrogramImage); UnloadTexture(app.spectrogramTexture); }
|
||||
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
|
||||
app.visibleTexture = (Texture2D){ 0 };
|
||||
@@ -1693,7 +1709,7 @@ int main(int argc, char* argv[])
|
||||
free(app.reassignBuffer);
|
||||
FreeMlnl(&app.annotations);
|
||||
FreeSignal(&app.signal);
|
||||
CloseAudioDevice();
|
||||
if (IsAudioDeviceReady()) CloseAudioDevice();
|
||||
CloseWindow();
|
||||
return headlessRc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user