Skip to content

anyrender_vello: allow windows to share one wgpu device and Vello renderer - #122

Open
Meirdrarel wants to merge 2 commits into
DioxusLabs:mainfrom
Meirdrarel:vello-shared-context
Open

Meirdrarel wants to merge 2 commits into
DioxusLabs:mainfrom
Meirdrarel:vello-shared-context

Conversation

@Meirdrarel

@Meirdrarel Meirdrarel commented Oct 19, 2026 •

Copy link
Copy Markdown

Fixes #121

Today every VelloWindowRenderer creates its own wgpu instance, device and vello::Renderer, so each
extra window costs ~180 MB of GPU memory on macOS (4 windows: 802 MB). This PR adds an opt-in way to
share them.

Changes

  • New SharedVelloContext (cheap to clone): one WGPUContext + one vello::Renderer per device.
  • New VelloWindowRenderer::with_options_and_shared_context(options, context).
    new() / with_options() are unchanged for callers: they create a private context.
  • Each window keeps only what is per window: its SurfaceRenderer (surface + intermediate texture), its scene and its custom-texture handles.
  • The device is added to the pool right after it's created, instead of in complete_resume.
    Otherwise windows resumed before the first one completes (every startup window) can't reuse it.

Results

macOS, M2 Max, Retina windows, Blitz app with N windows (macOS physical footprint):

Windows Before After (shared context)
1 223 MB 223 MB
2 424 MB 240 MB
4 802–821 MB 273 MB

Tested with Blitz (blitz-shell, several Views):

  • windows of different sizes rendering through the one shared vello::Renderer (1600x1200, 1000x536,
    1800x1336 in the same run), visually checked;
  • a window opened at runtime (after startup) reuses the device;
  • closing the window that created the device, then opening a new one, works;
  • closing windows releases their memory (257 → 235 MB going from 3 to 1 window).

cargo fmt, cargo clippy --workspace -- -D warnings, cargo doc and the CI's wasm32 clippy
checks (--target wasm32-unknown-unknown) are clean locally. Not run in a browser; the code uses Rc,
which is fine with spawn_local. Note that on
wasm the device is only registered once its async init resolves, so windows created in the same tick
still get their own device there, as before.

Behaviour notes

  • With a private context (the default), a suspend/resume now reuses the cached vello::Renderer for
    the device instead of building a new one. The device was already reused before, so this just
    avoids recompiling pipelines.
  • Windows sharing a context must use compatible options (features, limits and pipeline_cache
    are taken from the context / the first window). Per-window options like base_color and
    antialiasing_method still apply per window.

Follow-up

anyrender_vello_hybrid has the same structure; I can do the same there if this approach is OK.

…derer

Add `SharedVelloContext` and `VelloWindowRenderer::with_shared_context` so that
several windows reuse the same wgpu instance, device and `vello::Renderer`
instead of creating one each. `new()` / `with_options()` keep their behaviour
(they use a private context).

The new device is registered in the pool as soon as it is created rather than
in `complete_resume`, so windows resumed before the first one completes (all
startup windows) can find and reuse it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@HigherOrderLogic HigherOrderLogic left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative: #118.

/// and one Vello renderer per device (shaders, pipelines and Vello's internal buffers).
struct GpuState {
wgpu_context: WGPUContext,
renderers: Vec<(wgpu::Device, Rc<RefCell<VelloRenderer>>)>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cant this be a HashMap?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wgpu::Device implements Hash, so it could. I used a Vec because there's almost always one device (two on multi-GPU setups), where a linear scan is simpler and just as fast.
Happy to switch to FxHashMap if preferred.

Comment thread crates/anyrender_vello/src/window_renderer.rs Outdated
window_handle: Option<Arc<dyn WindowHandle>>,

wgpu_context: WGPUContext,
gpu: SharedVelloContext,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think forcing user to always hold a shared context is a good idea.

@Meirdrarel Meirdrarel Oct 20, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callers don't need to hold one: new() and with_options() are unchanged and create a private context internally. Only code that wants sharing creates a SharedVelloContext.
Internally the renderer always goes through the Rc<RefCell<…>>, which costs a refcount and a borrow check per resume.
An owned/shared enum would avoid that, but it needs a match at every use site.

self.window_handle = Some(window_handle.clone());

let surface = self
let gpu = self.gpu.clone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe consider a non-.clone() path here since cloning a Vec/HashMap is relatively expensive.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SharedVelloContext is a newtype around an Rc, so this clones a pointer, not the Vec. The clone is moved into the async init so it can register the new device in the shared pool as soon as it exists.

…d_context

Matches with_options (review suggestion).
@Meirdrarel

Copy link
Copy Markdown
Author

Thanks for the review! #118 tackles the same thing for vello_hybrid.
One difference that matters for memory: the device only enters device_pool in complete_resume, so windows resumed at startup each create their own device even with a shared context. This PR registers the device right after creation instead.
It would be good to settle on one API for both backends, and I'm happy to follow whichever shape the maintainers prefer.

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.

anyrender_vello: each window creates its own wgpu device and Vello renderer (~180 MB per window)

2 participants