use anyrender::{
RegisterResourceErrorKind, RenderContext, ResourceId, WindowHandle, WindowRenderer,
};
use debug_timer::debug_timer;
use futures_channel::oneshot;
use peniko::{Color, ImageData};
use rustc_hash::FxHashMap;
use std::future::Future;
use std::sync::Arc;
use vello::{
AaConfig, AaSupport, RenderParams, Renderer as VelloRenderer, RendererOptions,
Scene as VelloScene,
};
use wgpu::{Features, Limits, PresentMode, SurfaceError, Texture, TextureFormat, TextureUsages};
use wgpu_context::{
DeviceHandle, SurfaceRenderer, SurfaceRendererConfiguration, TextureConfiguration, WGPUContext,
};
use crate::{DEFAULT_THREADS, VelloScenePainter};
#[cfg(target_arch = "wasm32")]
fn spawn_init<F: Future<Output = ()> + 'static>(f: F) {
wasm_bindgen_futures::spawn_local(f);
}
#[cfg(not(target_arch = "wasm32"))]
fn spawn_init<F: Future<Output = ()>>(f: F) {
pollster::block_on(f);
}
struct ActiveRenderState {
renderer: VelloRenderer,
render_surface: SurfaceRenderer<'static>,
}
struct InitOutput {
active: ActiveRenderState,
}
#[allow(clippy::large_enum_variant)]
enum RenderState {
Suspended,
Pending {
receiver: oneshot::Receiver<InitOutput>,
},
Active(ActiveRenderState),
}
#[derive(Clone)]
pub struct VelloRendererOptions {
pub features: Option<Features>,
pub limits: Option<Limits>,
pub base_color: Color,
pub antialiasing_method: AaConfig,
}
impl Default for VelloRendererOptions {
fn default() -> Self {
Self {
features: None,
limits: None,
base_color: Color::WHITE,
antialiasing_method: AaConfig::Msaa16,
}
}
}
pub struct VelloWindowRenderer {
render_state: RenderState,
window_handle: Option<Arc<dyn WindowHandle>>,
wgpu_context: WGPUContext,
scene: VelloScene,
config: VelloRendererOptions,
texture_handles: FxHashMap<ResourceId, ImageData>,
}
impl VelloWindowRenderer {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::with_options(VelloRendererOptions::default())
}
pub fn with_options(config: VelloRendererOptions) -> Self {
Self {
render_state: RenderState::Suspended,
wgpu_context: build_wgpu_context(&config),
config,
window_handle: None,
scene: VelloScene::new(),
texture_handles: FxHashMap::default(),
}
}
pub fn current_device_handle(&self) -> Option<&DeviceHandle> {
match &self.render_state {
RenderState::Active(active) => Some(&active.render_surface.device_handle),
_ => None,
}
}
}
impl RenderContext for VelloWindowRenderer {
fn try_register_custom_resource(
&mut self,
resource: Box<dyn std::any::Any>,
) -> Result<ResourceId, anyrender::RegisterResourceError> {
let RenderState::Active(active) = &mut self.render_state else {
return Err(RegisterResourceErrorKind::NotActive.into());
};
if let Ok(texture) = resource.downcast::<Texture>() {
let id = ResourceId::new();
self.texture_handles
.insert(id, active.renderer.register_texture(*texture));
Ok(id)
} else {
Err(anyrender::RegisterResourceErrorKind::UnsupportedResourceKind.into())
}
}
fn unregister_resource(&mut self, resource_id: ResourceId) {
let RenderState::Active(active) = &mut self.render_state else {
return;
};
if let Some(handle) = self.texture_handles.remove(&resource_id) {
active.renderer.unregister_texture(handle);
}
}
fn renderer_specific_context(&self) -> Option<Box<dyn std::any::Any>> {
match &self.render_state {
RenderState::Active(active) => {
Some(Box::new(active.render_surface.device_handle.clone()))
}
RenderState::Pending { .. } => None,
RenderState::Suspended => None,
}
}
}
fn build_wgpu_context(config: &VelloRendererOptions) -> WGPUContext {
let features =
config.features.unwrap_or_default() | Features::CLEAR_TEXTURE | Features::PIPELINE_CACHE;
WGPUContext::with_features_and_limits(Some(features), config.limits.clone())
}
impl WindowRenderer for VelloWindowRenderer {
type ScenePainter<'a>
= VelloScenePainter<'a, 'a>
where
Self: 'a;
fn is_active(&self) -> bool {
matches!(self.render_state, RenderState::Active { .. })
}
fn is_pending(&self) -> bool {
matches!(self.render_state, RenderState::Pending { .. })
}
fn resume<F: FnOnce() + 'static>(
&mut self,
window_handle: Arc<dyn WindowHandle>,
width: u32,
height: u32,
on_ready: F,
) {
if !matches!(self.render_state, RenderState::Suspended) {
return;
}
let (sender, receiver) = oneshot::channel();
self.render_state = RenderState::Pending { receiver };
self.window_handle = Some(window_handle.clone());
let surface = self
.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 existing_device_handle = self
.wgpu_context
.find_compatible_device_handle(Some(&surface));
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"),
};
let render_surface = SurfaceRenderer::new(
surface,
SurfaceRendererConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
formats: vec![TextureFormat::Rgba8Unorm, TextureFormat::Bgra8Unorm],
width,
height,
present_mode: PresentMode::AutoVsync,
desired_maximum_frame_latency: 2,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
},
Some(TextureConfiguration {
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
}),
device_handle,
)
.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: None,
},
)
.unwrap();
let _ = sender.send(InitOutput {
active: ActiveRenderState {
renderer,
render_surface,
},
});
on_ready();
});
}
fn complete_resume(&mut self) -> bool {
match &mut self.render_state {
RenderState::Active { .. } => true,
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
}
_ => false,
},
}
}
fn suspend(&mut self) {
if let RenderState::Active(active) = &mut self.render_state {
for (_id, handle) in self.texture_handles.drain() {
active.renderer.unregister_texture(handle);
}
}
self.render_state = RenderState::Suspended;
}
fn set_size(&mut self, width: u32, height: u32) {
if let RenderState::Active(active) = &mut self.render_state {
active.render_surface.resize(width, height);
};
}
fn render<F: FnOnce(&mut Self::ScenePainter<'_>)>(&mut self, draw_fn: F) {
let RenderState::Active(state) = &mut self.render_state else {
return;
};
let render_surface = &mut state.render_surface;
debug_timer!(timer, feature = "log_frame_times");
draw_fn(&mut VelloScenePainter {
inner: &mut self.scene,
renderer: Some(&mut state.renderer),
device_handle: Some(&render_surface.device_handle),
texture_handles: Some(&mut self.texture_handles),
});
timer.record_time("cmd");
match render_surface.ensure_current_surface_texture() {
Ok(_) => {}
Err(SurfaceError::Timeout | SurfaceError::Lost | SurfaceError::Outdated) => {
render_surface.clear_surface_texture();
return;
}
Err(SurfaceError::OutOfMemory) => panic!("Out of memory"),
Err(SurfaceError::Other) => panic!("Unknown error getting surface"),
};
let texture_view = render_surface
.target_texture_view()
.expect("handled errors from ensure_current_surface_texture above");
state
.renderer
.render_to_texture(
render_surface.device(),
render_surface.queue(),
&self.scene,
&texture_view,
&RenderParams {
base_color: self.config.base_color,
width: render_surface.config.width,
height: render_surface.config.height,
antialiasing_method: self.config.antialiasing_method,
},
)
.expect("failed to render to texture");
timer.record_time("render");
drop(texture_view);
render_surface
.maybe_blit_and_present()
.expect("handled errors from ensure_current_surface_texture above");
timer.record_time("present");
render_surface
.device()
.poll(wgpu::PollType::wait_indefinitely())
.unwrap();
timer.record_time("wait");
timer.print_times("vello: ");
self.scene.reset();
}
}