Repository navigation
fix(render): end every render frame's GPU work so staging buffers are freed - #436
Merged
Merged
Conversation
A device-less daemon running a native effect on the GPU compositor grew by one canvas per frame in NVIDIA host mappings: RssShmem rose 1 GiB every 30 s at 30 fps (about 123 GiB/h) while anonymous memory stayed flat, and a reading full-resolution preview client did not stop it. The bypass compose path (sparkleflinger/gpu/compositor/bypass.rs) uploads every bypassed CPU frame into the front texture with Queue::write_texture so GPU sampling can read it, which it has done since 72d9149 (keep bypassed outputs sampleable). wgpu-core 30 keeps the staging behind each queue write in the queue's pending writes until the next Queue::submit, and polling the device never releases it. create_buffer_init on a buffer without MAP_WRITE stages the same way when it unmaps, which composed layer uploads and uniform ring overflow use. With LED zones the GPU sampler submits every frame and carries all of it. With no devices there is no sampling plan, a full-size preview takes the CPU canvas directly, and an unconsumed composition's staged encoder is superseded and dropped unsubmitted, so nothing submits and the staging is never freed. The GPU frame backpressure already ends the render loop's normal path with an empty submission, which hands wgpu every queue write the frame made whichever API staged it. This adds the same frame end on each early exit that can follow GPU work (scene transaction service, sleep and idle throttles, the late sleep switch, paused-loop transactions) and at the end of each interactive preview frame. Frame ends keep only the newest marks, so the preview compositor, which never waits on them, does not accumulate them. Regression tests count wgpu's "(wgpu internal) Staging" allocations over 48 frames: with end_gpu_frame emptied 49 stay live for each bypass case, 98 for two changing composed layers, and 51 for a static base under an animated overlay; with it all return to baseline. On an isolated preview daemon with Rainbow and no devices, RssShmem holds flat over 5.5 minutes with no clients (67.7 MB) and with a full-resolution preview reader (81.9 MB) after warm-up. Fixes #399 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ests The Studio layout persistence tests compile only with the persistence-test-hooks feature, which the CI clippy lanes do not enable, so a pedantic assigning_clones finding there went unnoticed until clippy ran with that feature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A daemon with no LED devices, running a builtin effect on the GPU compositor, leaked one full canvas of GPU staging memory per frame. In the reproduction, shared memory grew 1 GiB every 30 seconds at 30 fps, about 123 GiB an hour. A brand-new install with nothing plugged in yet is exactly that configuration. The leak has been present since
72d914939(April), so 0.7.0 ships it. With this PR every render frame hands its queue writes to wgpu before it ends, and memory holds flat.What was leaking
wgpu-core keeps the staging buffer behind each
Queue::write_textureandwrite_bufferin the queue's pending writes until the nextQueue::submit. Polling the device does not release it.create_buffer_initon a buffer withoutMAP_WRITEstages the same way when it unmaps.The compositor's bypass path uploads every bypassed CPU frame into the front texture with
write_texture, so GPU sampling can read it. With LED zones present, the GPU sampler submits every frame and carries those writes along. With no devices there is nothing to sample. A full-size preview takes the CPU canvas directly, and an unconsumed composition's encoder is dropped without being submitted. Nothing ever submits, so the staging is never freed. The composed path leaks the same way through layer uploads and the uniform ring.What this changes
The GPU frame backpressure in #432 already ends the render loop's normal path with an empty submission, which flushes every pending write no matter which API staged it. This PR adds that same frame end everywhere a frame can stop after doing GPU work:
execute_framethat can follow GPU work: scene transaction service, the sleep and idle throttles, and the late sleep switch;pipeline_driver.rs, after it services scene transactions;Frame ends now keep only the newest two marks. The interactive preview compositor never waits on its marks, so without the cap they would accumulate.
An earlier version counted queue writes and submitted only when writes were outstanding. Independent verification failed it, because
create_buffer_initstaging never passed through the counter: a static base layer under an animated overlay, with nothing else submitting, still leaked. The unconditional empty submit covers every staging path, and it costs about 4 µs a frame.Verification
just verifypasses locally (Rust fmt + lint + test)just denypasses (required for dependency or license changes)just ui-testandjust ui-buildpass (required forcrates/hypercolor-ui/)just sdk-lint,just sdk-check, andjust sdk-buildpass (required forsdk/)just python-verifypasses (required forpython/)just compat-checkpasses (required fordata/drivers/vendors/*.toml)just docs-buildpasses (required for docs or README changes)cd docs && zola checkpasses (required for docs link/content changes)scripts/orpackaging/)just e2e-buildpasses with the normal Servo stack (required for daemon/UI/effect integration changes)just e2e-build-cpupasses when validating the CPU smoke fallbackjust e2epasses against the Servo stack (required for end-to-end behavior changes; starts daemon/browser)Five regression tests count wgpu's live "(wgpu internal) Staging" allocations, through
Device::generate_allocator_report, across 48 frames of changing content. With the frame end emptied, they fail:With the frame end in place, all five return to baseline. The allocator report exists on Vulkan and DX12, so the WARP lane runs them; on backends without one, the tests skip.
cargo test -p hypercolor-daemon --lib -- --test-threads=1 render_thread(HYPERCOLOR_REQUIRE_GPU_TESTS=1)cargo test -p hypercolor-daemon --features persistence-test-hooks --test render_thread_tests -- --test-threads=1cargo clippy -p hypercolor-daemon --all-targets -- -D warnings,--no-default-features --lib, and--no-default-features --features wgpu,persistence-test-hooks --all-targetscargo fmt --all -- --checkThe last clippy shape also caught a pedantic
assigning_clonesfinding in the Studio layout persistence tests. That file compiles only withpersistence-test-hooks, which the CI clippy lanes don't enable, so the finding went unnoticed. It's fixed in a separate commit here.An isolated preview daemon ran Rainbow with no devices for 5.5 minutes per scenario, in a sandbox with no hardware nodes. Before the fix, shared memory grew about 35 MiB/s with no clients. After it:
With the reader, the one-time step happens when it attaches; memory is flat after that. An independent verifier passed this version after failing the counted one. That issue's other suspected trigger, stalled WebSocket clients, was ruled out with 15 to 20-minute soaks, including four stalled clients at once.
Notes for reviewers
The September 30 report in #399 grew anonymous memory, not shared memory, and dropped suddenly. That pattern matches the Servo script leak and starved garbage collection fixed in 0.7.0 better than it matches this leak. This PR fixes the reproducible leak that issue's investigation turned up. If anonymous growth like the original report ever reappears, it belongs in a new issue.
🤖 Generated with Claude Code
Summary by CodeRabbit