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;
|
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)
|
void PlaySelectedRegion(void)
|
||||||
{
|
{
|
||||||
int numSamples = 0;
|
int numSamples = 0;
|
||||||
float* regionSamples = BuildSelectionAudio(&numSamples);
|
float* regionSamples = BuildSelectionAudio(&numSamples);
|
||||||
if (!regionSamples) return;
|
if (!regionSamples) return;
|
||||||
|
|
||||||
|
EnsureAudioDevice(); // opened on demand; released again once playback ends
|
||||||
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
||||||
|
|
||||||
Wave wave = { .data = regionSamples, .frameCount = (unsigned int)numSamples,
|
Wave wave = { .data = regionSamples, .frameCount = (unsigned int)numSamples,
|
||||||
|
|||||||
@@ -10,8 +10,16 @@ bool LoadWavFile(const char* filepath, AudioSignal* signal);
|
|||||||
void FreeSignal(AudioSignal* signal);
|
void FreeSignal(AudioSignal* signal);
|
||||||
|
|
||||||
// Play the current time/frequency selection (applies an FFT bandpass filter).
|
// Play the current time/frequency selection (applies an FFT bandpass filter).
|
||||||
|
// Opens the audio output device on demand (see EnsureAudioDevice).
|
||||||
void PlaySelectedRegion(void);
|
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
|
// Save the current selection (band-limited, time-cropped — same processing as
|
||||||
// playback) as a mono WAV in dirPath. Reports the result via app.exportMessage.
|
// playback) as a mono WAV in dirPath. Reports the result via app.exportMessage.
|
||||||
void ExportSelectionWAV(const char* dirPath);
|
void ExportSelectionWAV(const char* dirPath);
|
||||||
|
|||||||
+43
-27
@@ -70,12 +70,11 @@ static bool IsUserInteracting(void)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle FPS throttle. raylib re-renders the whole scene every frame regardless
|
// Idle power management. raylib re-renders the whole scene every frame, so an
|
||||||
// of whether anything changed, so an idle window still pins the GPU (and, on a
|
// idle window otherwise pins the GPU (and, on software GL, the CPU) at the
|
||||||
// software GL stack, the CPU) at 60 fps. We drop to IDLE_FPS whenever nothing
|
// target rate. We run at ACTIVE_FPS while something needs animating, then go
|
||||||
// needs animating, and snap back to ACTIVE_FPS the moment activity resumes.
|
// fully event-driven (block until input) when idle — see the loop below.
|
||||||
#define ACTIVE_FPS 60
|
#define ACTIVE_FPS 30 // plenty for a non-game UI; halves active-frame cost
|
||||||
#define IDLE_FPS 10
|
|
||||||
#define IDLE_GRACE_SECONDS 0.5 // stay at full rate briefly after the last activity
|
#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
|
#endif
|
||||||
InitWindow(headless ? reqW : 1280, headless ? reqH : 800, "Spectrogram Viewer");
|
InitWindow(headless ? reqW : 1280, headless ? reqH : 800, "Spectrogram Viewer");
|
||||||
SetTargetFPS(60);
|
SetTargetFPS(ACTIVE_FPS);
|
||||||
SetTraceLogLevel(LOG_WARNING); // Suppress INFO texture logs
|
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
|
SetExitKey(KEY_NULL); // ESC should not close the window
|
||||||
|
|
||||||
// Save original working directory so command-line args resolve correctly
|
// 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).
|
// Track the browser viewport (fill + reflow on resize, like desktop).
|
||||||
SyncCanvasToWindow();
|
SyncCanvasToWindow();
|
||||||
#else
|
#else
|
||||||
// When the window isn't focused there's nothing for the user to see, so
|
// Desktop power management. raylib's frame limiter partial-busy-waits
|
||||||
// skip the entire frame: pump window events (so focus regain and the
|
// ~5% of every frame interval, so capping the FPS can't get idle CPU
|
||||||
// close button still register) and idle, with zero drawing. The
|
// below ~5% of a core. Instead we go fully event-driven when idle:
|
||||||
// headless render uses a hidden window that never reports focus, so it
|
// block in PollInputEvents() (glfwWaitEvents) until an input/window
|
||||||
// is exempt — otherwise it would never draw its single capture frame.
|
// event arrives, so an idle/backgrounded window costs ~0% CPU and only
|
||||||
if (!headless && !IsWindowFocused()) {
|
// 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 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();
|
PollInputEvents();
|
||||||
WaitTime(0.1); // ~10 Hz event poll, no rendering
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Focused but idle: fall through to draw this one static frame,
|
||||||
// Idle FPS throttle (desktop only; the browser drives RAF/vsync itself).
|
// then EndDrawing blocks until the next event.
|
||||||
// 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
|
#endif
|
||||||
|
|
||||||
@@ -1682,7 +1698,7 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
TraceLog(LOG_INFO, "Shutting down...");
|
TraceLog(LOG_INFO, "Shutting down...");
|
||||||
if (mainFont.texture.id != 0) UnloadFont(mainFont);
|
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.stftComputed) { FreeSTFT(&app.stft); UnloadImage(app.spectrogramImage); UnloadTexture(app.spectrogramTexture); }
|
||||||
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
|
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
|
||||||
app.visibleTexture = (Texture2D){ 0 };
|
app.visibleTexture = (Texture2D){ 0 };
|
||||||
@@ -1693,7 +1709,7 @@ int main(int argc, char* argv[])
|
|||||||
free(app.reassignBuffer);
|
free(app.reassignBuffer);
|
||||||
FreeMlnl(&app.annotations);
|
FreeMlnl(&app.annotations);
|
||||||
FreeSignal(&app.signal);
|
FreeSignal(&app.signal);
|
||||||
CloseAudioDevice();
|
if (IsAudioDeviceReady()) CloseAudioDevice();
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
return headlessRc;
|
return headlessRc;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user