Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/anyrender_vello/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod window_renderer;
pub use image_renderer::VelloImageRenderer;
pub use scene::VelloScenePainter;
pub use wgpu_context::DeviceHandle;
pub use window_renderer::{VelloRendererOptions, VelloWindowRenderer};
pub use window_renderer::{SharedVelloContext, VelloRendererOptions, VelloWindowRenderer};

pub use wgpu;

Expand Down
139 changes: 104 additions & 35 deletions crates/anyrender_vello/src/window_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use debug_timer::debug_timer;
use futures_channel::oneshot;
use peniko::{Color, ImageData};
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::future::Future;
use std::rc::Rc;
use std::sync::Arc;
use vello::{
AaConfig, AaSupport, RenderParams, Renderer as VelloRenderer, RendererOptions,
Expand Down Expand Up @@ -37,10 +39,41 @@ fn spawn_init<F: Future<Output = ()>>(f: F) {
}

struct ActiveRenderState {
renderer: VelloRenderer,
renderer: Rc<RefCell<VelloRenderer>>,
render_surface: SurfaceRenderer<'static>,
}

/// GPU state that can be shared by several windows: one wgpu instance, its device pool,
/// 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.

}

impl GpuState {
fn renderer_for(&self, device: &wgpu::Device) -> Option<Rc<RefCell<VelloRenderer>>> {
self.renderers
.iter()
.find(|(d, _)| d == device)
.map(|(_, renderer)| Rc::clone(renderer))
}
}

/// A GPU context shared between windows. Pass it to
/// [`VelloWindowRenderer::with_options_and_shared_context`] for every window so they reuse the same
/// wgpu device and Vello renderer instead of creating one each.
#[derive(Clone)]
pub struct SharedVelloContext(Rc<RefCell<GpuState>>);

impl SharedVelloContext {
pub fn new(config: &VelloRendererOptions) -> Self {
Self(Rc::new(RefCell::new(GpuState {
wgpu_context: build_wgpu_context(config),
renderers: Vec::new(),
})))
}
}

/// Result of a successful asynchronous resume; both the active state and the
/// `WGPUContext` are returned so the renderer can reclaim the context.
struct InitOutput {
Expand Down Expand Up @@ -161,7 +194,7 @@ pub struct VelloWindowRenderer {
render_state: RenderState,
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.

scene: VelloScene,
config: VelloRendererOptions,

Expand All @@ -176,10 +209,21 @@ impl VelloWindowRenderer {
}

pub fn with_options(config: impl Into<VelloRendererOptions>) -> Self {
let config = config.into();
let gpu = SharedVelloContext::new(&config);
Self::with_options_and_shared_context(config, gpu)
}

/// Create a renderer that uses `gpu` for its device and Vello renderer, so that
/// several windows can share them. `gpu` should be created with compatible options.
pub fn with_options_and_shared_context(
config: impl Into<VelloRendererOptions>,
gpu: SharedVelloContext,
) -> Self {
let config = config.into();
Self {
render_state: RenderState::Suspended,
wgpu_context: build_wgpu_context(&config),
gpu,
config,
window_handle: None,
scene: VelloScene::new(),
Expand Down Expand Up @@ -207,7 +251,7 @@ impl RenderContext for VelloWindowRenderer {
if let Ok(texture) = resource.downcast::<Texture>() {
let id = ResourceId::new();
self.texture_handles
.insert(id, active.renderer.register_texture(*texture));
.insert(id, active.renderer.borrow_mut().register_texture(*texture));
Ok(id)
} else {
Err(anyrender::RegisterResourceErrorKind::UnsupportedResourceKind.into())
Expand All @@ -220,7 +264,7 @@ impl RenderContext for VelloWindowRenderer {
};

if let Some(handle) = self.texture_handles.remove(&resource_id) {
active.renderer.unregister_texture(handle);
active.renderer.borrow_mut().unregister_texture(handle);
}
}

Expand Down Expand Up @@ -276,13 +320,15 @@ impl WindowRenderer for VelloWindowRenderer {
self.render_state = RenderState::Pending { receiver };
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.

let mut gpu_state = gpu.0.borrow_mut();
let surface = gpu_state
.wgpu_context
.create_surface(window_handle)
.expect("Error creating surface");
let instance = self.wgpu_context.instance.clone();
let extra_features = self.wgpu_context.extra_features();
let override_limits = self.wgpu_context.override_limits();
let instance = gpu_state.wgpu_context.instance.clone();
let extra_features = gpu_state.wgpu_context.extra_features();
let override_limits = gpu_state.wgpu_context.override_limits();
let mut composite_alpha_mode = match self.config.composite_alpha_mode {
anyrender::CompositeAlphaMode::Auto => CompositeAlphaMode::Auto,
anyrender::CompositeAlphaMode::Opaque => CompositeAlphaMode::Opaque,
Expand All @@ -301,21 +347,32 @@ impl WindowRenderer for VelloWindowRenderer {
};
let pipeline_cache = self.config.pipeline_cache.clone();
let desired_maximum_frame_latency = self.config.desired_maximum_frame_latency;
let existing_device_handle = self
let existing_device_handle = gpu_state
.wgpu_context
.find_compatible_device_handle(Some(&surface));
drop(gpu_state);

spawn_init(async move {
let device_handle = match existing_device_handle {
Some(device_handle) => device_handle,
None => DeviceHandle::new_from_compatible_surface(
instance,
Some(&surface),
extra_features,
override_limits,
)
.await
.expect("Error creating DeviceHandle"),
None => {
let device_handle = DeviceHandle::new_from_compatible_surface(
instance,
Some(&surface),
extra_features,
override_limits,
)
.await
.expect("Error creating DeviceHandle");
// Register now (not in `complete_resume`) so that windows resumed before
// this one completes, e.g. all startup windows, can reuse the device
gpu.0
.borrow_mut()
.wgpu_context
.device_pool
.push(device_handle.clone());
device_handle
}
};

let adapter = &device_handle.adapter;
Expand Down Expand Up @@ -382,16 +439,30 @@ impl WindowRenderer for VelloWindowRenderer {
)
.expect("Error creating SurfaceRenderer");

let renderer = VelloRenderer::new(
render_surface.device(),
RendererOptions {
antialiasing_support: AaSupport::all(),
use_cpu: false,
num_init_threads: DEFAULT_THREADS,
pipeline_cache,
},
)
.unwrap();
// Reuse the Vello renderer of this device if another window already created it
let existing_renderer = gpu.0.borrow().renderer_for(render_surface.device());
let renderer = match existing_renderer {
Some(renderer) => renderer,
None => {
let renderer = Rc::new(RefCell::new(
VelloRenderer::new(
render_surface.device(),
RendererOptions {
antialiasing_support: AaSupport::all(),
use_cpu: false,
num_init_threads: DEFAULT_THREADS,
pipeline_cache,
},
)
.unwrap(),
));
gpu.0
.borrow_mut()
.renderers
.push((render_surface.device().clone(), Rc::clone(&renderer)));
renderer
}
};

let _ = sender.send(InitOutput {
active: ActiveRenderState {
Expand All @@ -409,8 +480,6 @@ impl WindowRenderer for VelloWindowRenderer {
RenderState::Suspended => false,
RenderState::Pending { receiver } => match receiver.try_recv() {
Ok(Some(InitOutput { active })) => {
let device_handle = active.render_surface.device_handle.clone();
self.wgpu_context.device_pool.push(device_handle);
self.render_state = RenderState::Active(active);
true
}
Expand All @@ -423,7 +492,7 @@ impl WindowRenderer for VelloWindowRenderer {
if let RenderState::Active(active) = &mut self.render_state {
// Unregister all textures on suspend
for (_id, handle) in self.texture_handles.drain() {
active.renderer.unregister_texture(handle);
active.renderer.borrow_mut().unregister_texture(handle);
}
}
self.render_state = RenderState::Suspended;
Expand All @@ -441,13 +510,14 @@ impl WindowRenderer for VelloWindowRenderer {
};

let render_surface = &mut state.render_surface;
let mut renderer = state.renderer.borrow_mut();

debug_timer!(timer, feature = "log_frame_times");

// Regenerate the vello scene
draw_fn(&mut VelloScenePainter {
inner: &mut self.scene,
renderer: Some(&mut state.renderer),
renderer: Some(&mut renderer),
device_handle: Some(&render_surface.device_handle),
texture_handles: Some(&mut self.texture_handles),
});
Expand All @@ -461,11 +531,10 @@ impl WindowRenderer for VelloWindowRenderer {
};

for handle in self.texture_handles.values() {
state.renderer.mark_override_image_dirty(handle);
renderer.mark_override_image_dirty(handle);
}

state
.renderer
renderer
.render_to_texture(
render_surface.device(),
render_surface.queue(),
Expand Down