Eliminating preset-switch stutter: preload API, background shader compile, and a transpile cache (offering several PRs) #1008
keeper-of-memes
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Hi! I build RoonVis, a native tvOS (Apple TV 4K) music visualiser built on libprojectM, rendered through ANGLE (OpenGL ES 3.0 on Metal) and driven by live audio from a Snapcast stream. Getting it to hold a smooth 60 fps on an A15 exposed a few systemic costs in projectM's preset-switch path. I've root-caused and fixed them locally, measured everything on real hardware, and I'd like to contribute the reusable parts back. Branches are ready. I'm opening this discussion first so you can tell me what you'd take and in what shape.
The problem
Two distinct stutters, measured with a frame profiler split across projectM's transition path:
projectm_load_preset_file()creates AND initialises the preset inside the switch, so complex presets stall the render thread 600–1200 ms at the exact moment the crossfade begins (Instruments independently showed 1.0–1.3 s main-thread hangs).What I'd like to upstream
1. Preset preload/activate API (+ optional background shader compile)
Branch:
upstream-preset-preload-api(~540 lines, all opt-in, default behaviour unchanged)Apps that own their rotation (know current/next ahead of time) can fully create and initialise the next preset during the dwell, then activate it instantly, so the switch no longer pays the init cost. Activation validates the window size (discards and returns false on mismatch so the caller falls back to a normal load), and texture purging moves to the handoff so a speculative preload can't evict textures the active preset still uses.
A finding you may want even if you take nothing else: ANGLE (and other drivers with
GL_KHR_parallel_shader_compile) backgroundsglCompileShader/glLinkProgramon a worker pool. However,Shader::CompileProgramcallsglDetachShaderimmediately afterglLinkProgram, and ANGLE'sProgram::detachShadercallsresolveLink()first, which synchronously blocks on the whole link plus all Metal shader-library compile subtasks. That one call defeats the extension entirely. During preload my branch defers everything pastglLinkProgram(status checks AND detach/delete) behind a per-Shaderpending state; readiness is polled with the non-resolvingGL_COMPLETION_STATUS_KHR, activation finalises with the same failure fallbacks as the sync path, andBind()/Validate()lazily finalise as a safety net. On drivers with no background compile the deferral is harmless.Measured (Apple TV 4K, A15): preset-switch init spike 651 → 133 ms average with preload alone;
glLinkProgramrender-thread cost 33–55 ms → 0.04–0.10 ms with the deferral, background link completing over 1–4 frames, finalise at activation 0.003–3.1 ms. A 25-preset sweep had every preload ready before its switch, 0 fallbacks, 0 link errors. Also validated over a 45-minute burn-in with no leaks and crossfade behaviour untouched.2. Opt-in preprocessed-HLSL cache hooks
Branch:
upstream-preprocess-cache-hooks(~280 lines net of a mechanical extraction; zero behaviour change when unused)The hlslparser preprocessor output is a pure, deterministic function of its input text (I verified byte-identical output with a golden-output test harness), so it's cacheable across runs, and even precomputable at build time for a bundled preset pack. The branch extracts the preprocessor-input assembly verbatim into a GL-free free function (
AssembleApplyPreprocessorInput) so external tooling produces byte-identical input, derives a version-salted 128-bit key from the exact input bytes, and consults the app's hooks aroundApplyPreprocessor. A stale or missing entry just misses and runs the live preprocessor, so it can never produce wrong output. projectM stores no cache itself; policy and persistence are entirely the app's.Measured: first-load transpile 177 → ~77 ms with a build-time prepopulated cache (RoonVis ships one for its bundled pack, making the first-ever load of every preset a hit).
3. Two small standalone fixes
upstream-glresolver-user-resolver-fallback(~14 lines): when the current GL context can't be probed via native APIs but the app supplied a resolver viaprojectm_create_with_opengl_load_proc(), assume an app-managed EGL/GLES context and continue instead of hard-failing. This is what makes projectM initialise under ANGLE on tvOS/iOS (app-owned EGL over Metal, no system EGL to query). No behaviour change without a user resolver.upstream-cmake-project-version(1 line):PROJECTM_LIB_VERSIONusedCMAKE_PROJECT_VERSION, which picks up the parent project's version when libprojectM is built viaadd_subdirectory/FetchContent;PROJECT_VERSIONis correct in both standalone and embedded builds.A question: OpenGL ES 3.0 floor
GladLoadercurrently requires ES 3.2 / GLSL ES 3.20. I run projectM on ES 3.0 (ANGLE on Metal) with a 292-preset curated pack: warp/composite shaders, blur and motion vectors all work, and I haven't hit a 3.1+ dependency in practice. Would you be open to lowering the floor to 3.0 (perhaps behind a probe/flag)? Happy to do the API audit and send a PR if so; otherwise I'll keep it as a local patch.Related
I have a separate PR open: #1007 (beat detection uses both channels instead of left-only).
All four branches sit on current
master, build clean on macOS, and pass the unit tests. I'm happy to reshape any of this (split differently, rename the API, add tests, target a feature branch) to fit the project's direction. Which parts would you like as PRs?All reactions