Skip to content

winit-host: flush queued writes when the frame cannot be acquired - #5

Merged
mtdowling merged 1 commit into
tecs-dev:mainfrom
albanmgicquel:fix-occluded-leak
Oct 12, 2026
Merged

mtdowling merged 1 commit into
tecs-dev:mainfrom
albanmgicquel:fix-occluded-leak

Conversation

@albanmgicquel

Copy link
Copy Markdown
Collaborator

Problem

Graphics::render queues the frame's buffer writes (instances, batches, cull uniform, …) and only then acquires the surface texture. When the acquisition returns Timeout or Occluded it returns Ok(false) without submitting, so every write_buffer of that frame stays pending in the queue with its own staging buffer. The same happens on Outdated/Lost.

While a window stays hidden — minimised, fully covered, on another Space, or with the display asleep — this repeats every frame and memory grows without bound. In a 320×200 sprite scene it grows about 50 MB/s, in both malloc and IOAccelerator (GPU) regions, until the machine runs out of memory. A visible window is unaffected, which is why it is easy to miss.

malloc_history on a hidden window after 25 s showed ~108,000 live allocations from wgpu_hal::metal::Device::create_buffer and a 71 MB pending-writes vector in wgpu_core::device::queue::Queue::write_buffer, while the scene itself (entities, image uploads, Lua heap) was constant.

Fix

Submit an empty batch before returning when no frame can be acquired (Timeout, Occluded), and before reconfiguring on Outdated/Lost. The writes are flushed (they are deltas the next frame relies on, so dropping them would be wrong), nothing is drawn, and wgpu reclaims the staging buffers.

Testing

  • cargo fmt --all -- --check, cargo clippy -p tecs-winit-host --all-targets -- -D warnings, cargo test -p tecs-winit-host (102 passed).
  • nupp task verify: the Nupp suites pass (997 tests in 86 suites); the documentation step could not run in my environment (the lunamark rock is missing from my from-source Nupp toolchain), so the gate stopped there — the Rust gates above were run directly.
  • Manual: a game component minimised and restored twice, sampling RSS every 10 s — flat at 136–152 MB with the fix, versus ~50 MB/s growth (2 GB in under a minute) without it.

No automated test: the failing path needs a real window that the OS has occluded, which the offscreen host cannot produce.

A Timeout or Occluded surface returned before any submit, leaving every
write_buffer of the frame pending in the queue. While a window stays
hidden or occluded that grows without bound (about 50 MB/s in a sprite
scene: one staging buffer per write). Submitting an empty batch flushes
them; Outdated/Lost do the same before reconfiguring.
@mtdowling
mtdowling merged commit 40e234f into tecs-dev:main Oct 12, 2026
6 of 8 checks passed
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.

3 participants