Skip to content

Add preset preload API with optional background shader compilation (targeting 4.3)#1012

Draft
keeper-of-memes wants to merge 1 commit into
projectM-visualizer:masterfrom
keeper-of-memes:upstream-preset-preload-api
Draft

Add preset preload API with optional background shader compilation (targeting 4.3)#1012
keeper-of-memes wants to merge 1 commit into
projectM-visualizer:masterfrom
keeper-of-memes:upstream-preset-preload-api

Conversation

@keeper-of-memes

Copy link
Copy Markdown

Targeting 4.3 (per the maintainer's guidance in the preset-switch performance discussion). Will rebase once the remaining 4.2 items (#970, #971, #1003) have merged.

Problem

projectm_load_preset_file() creates AND initialises the preset inside the switch, so complex Milkdrop presets stall the render thread for hundreds of milliseconds (600–1200 ms measured on an Apple TV 4K / A15) at the exact moment the transition starts. Applications that own their preset rotation, and therefore know the next preset ahead of time, have no way to prepare it in advance.

New C API (all opt-in; existing behaviour unchanged)

  • projectm_preload_preset_file(): create and fully initialise a preset (including shader compilation) without switching to it. One preset can be staged at a time; a subsequent call replaces it. On failure the preset-switch-failed callback fires and nothing is retained.
  • projectm_activate_preloaded_preset(): switch to the staged preset, skipping re-initialisation. Returns false (caller falls back to a normal load) if nothing is staged or the window size changed since the preload. Initialize() bakes the surface size into GL resources, so a stale preload is discarded.
  • projectm_has_preloaded_preset(): query whether a preset is staged.
  • projectm_preloaded_preset_compile_ready(): non-blocking readiness poll for the deferred background shader compiles (below).

Texture purging moves to activation (not preload) so a speculative preload can't age or evict textures the active preset still uses.

Background shader compilation

During preload, Renderer::Shader defers everything past glLinkProgram. Drivers that implement GL_KHR_parallel_shader_compile (ANGLE does) background the compile/link on a worker pool, but ANY program-observing call synchronously resolves the link on the calling thread. That includes the glDetachShader immediately after glLinkProgram in the normal path: in ANGLE, Program::detachShader calls resolveLink(), which blocks on the main link job plus all Metal shader-library compile subtasks. With the deferral, the link keeps running during the preload lead time; readiness is polled via the non-resolving GL_COMPLETION_STATUS_KHR query, and activation finalises the program (status check, detach/delete, and the same per-shader failure fallbacks as the synchronous path). Bind()/Validate() finalise lazily as a safety net, so a pending shader is always safe to use. On drivers without background compilation the deferral is harmless: the link is simply already complete at finalise time.

Measurements (tvOS/ANGLE-Metal, Apple TV 4K / A15)

metric before after
preset-switch init spike (avg) ~651 ms ~133 ms (preload alone)
glLinkProgram on render thread ~33–55 ms 0.04–0.10 ms (deferred)
background link completion n/a 1–4 frames
finalise at activation n/a 0.003–3.1 ms

25-preset sweep: every preload ready before its switch, 0 fallbacks, 0 link errors. 45-minute burn-in: no leaks, crossfade behaviour untouched.

Notes for reviewers

  • All additions are opt-in; nothing changes for apps that only call projectm_load_preset_file().
  • Shader::SetDeferCompilation() is a render/GL-thread-only static flag, set only inside PreloadPresetFile() via a scope guard.
  • Preset::PendingShaderCompileReady()/FinalizePendingShaderCompile() are virtuals with safe defaults (true/no-op), overridden by MilkdropPreset to aggregate warp and composite.
  • Builds clean on macOS (Release), unit tests pass. Developed and validated on tvOS/ANGLE; needs eyes from Linux/Windows GL folks mainly for the deferred path on non-ANGLE drivers (expected harmless, since the KHR query is an optional fast path only).

Applications that own their preset rotation (know current/next ahead of
time) previously had no way to prepare the next preset before switching:
projectm_load_preset_file() creates AND initialises the preset inside
the switch, so complex Milkdrop presets stall the render thread for
hundreds of milliseconds (600-1200 ms measured on an Apple TV 4K / A15)
at the exact moment the transition starts.

New C API (all opt-in; existing behaviour unchanged):

- projectm_preload_preset_file(): create and fully initialise a preset
  (including shader compilation) without switching to it. One preset
  can be staged at a time.
- projectm_activate_preloaded_preset(): switch to the staged preset,
  skipping re-initialisation. Returns false (caller falls back to a
  normal load) if nothing is staged or the window size changed since
  the preload.
- projectm_has_preloaded_preset(): query whether a preset is staged.
- projectm_preloaded_preset_compile_ready(): non-blocking readiness
  poll for the deferred background shader compiles (below).

During preload, Renderer::Shader defers everything past glLinkProgram.
Drivers that implement GL_KHR_parallel_shader_compile (ANGLE does)
background the compile/link on a worker pool, but ANY program-observing
call synchronously resolves the link on the calling thread. That
includes the glDetachShader immediately after glLinkProgram in the
normal path. With the deferral, the link keeps running in the
background during the preload lead time; readiness is polled via the
non-resolving GL_COMPLETION_STATUS_KHR query, and activation finalises
the program (status check, detach/delete, and the same per-shader
failure fallbacks as the synchronous path). Bind()/Validate() finalise
lazily as a safety net, so a pending shader is always safe to use. On
drivers without background compilation the deferral is harmless; the
link is simply already complete at finalise time.

Texture purging moves to activation (not preload) so a speculative
preload can't age/evict textures the active preset still uses.

Measured on tvOS/ANGLE (Metal backend): preset-switch initialisation
spike drops from ~651 ms to ~133 ms average with preload alone;
with deferred compilation the render-thread cost of glLinkProgram
drops from ~33-55 ms to 0.04-0.10 ms per preset, with the background
link completing over 1-4 frames and finalisation at activation taking
0.003-3.1 ms.
@keeper-of-memes

Copy link
Copy Markdown
Author

Update from production use: the preload + deferred-compile path is now also running on Apple TV HD (A8) hardware. Two observations relevant to review: (1) the mechanism degrades gracefully on much slower GPUs — background compiles simply take more frames to report ready, and activation still finalises quickly; (2) on constrained devices the application benefits from throttling how much preload work happens per frame, which is entirely app-side policy — no API change needed, the non-blocking readiness poll is sufficient. Still targeting 4.3; will rebase after #970/#971/#1003.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant