use crate::camera::Camera;
use crate::renderer::ViewportId;
pub mod gpu_phase {
pub const PRE_PREPARE: i32 = 100;
pub const _RESERVED_INTERNAL: i32 = 500;
pub const POST_PAINT: i32 = 900;
}
#[non_exhaustive]
pub struct PostPaintTargets<'a> {
pub color_view: &'a wgpu::TextureView,
pub depth_view: &'a wgpu::TextureView,
pub pick_id_view: Option<&'a wgpu::TextureView>,
pub color_format: wgpu::TextureFormat,
}
impl<'a> PostPaintTargets<'a> {
pub fn new(
color_view: &'a wgpu::TextureView,
depth_view: &'a wgpu::TextureView,
color_format: wgpu::TextureFormat,
) -> Self {
Self {
color_view,
depth_view,
pick_id_view: None,
color_format,
}
}
pub fn with_pick_id_view(mut self, pick_id_view: &'a wgpu::TextureView) -> Self {
self.pick_id_view = Some(pick_id_view);
self
}
}
#[non_exhaustive]
pub struct GpuFrameContext<'a> {
pub camera: &'a Camera,
pub viewport_size: glam::Vec2,
pub dt: f32,
pub frame_index: u64,
pub viewport_id: Option<ViewportId>,
}
impl<'a> GpuFrameContext<'a> {
pub fn new(camera: &'a Camera, viewport_size: glam::Vec2, dt: f32, frame_index: u64) -> Self {
Self {
camera,
viewport_size,
dt,
frame_index,
viewport_id: None,
}
}
pub fn with_viewport(mut self, viewport_id: ViewportId) -> Self {
self.viewport_id = Some(viewport_id);
self
}
}
pub trait GpuPlugin: Send + 'static {
fn priority(&self) -> i32 {
gpu_phase::PRE_PREPARE
}
fn init_gpu(&mut self, _device: &wgpu::Device) {}
fn on_device_recreated(&mut self, _device: &wgpu::Device, _queue: &wgpu::Queue) {}
fn pre_prepare(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
Vec::new()
}
fn post_paint(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_targets: &PostPaintTargets<'_>,
_ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
Vec::new()
}
}
pub(super) struct ViewportScopedPlugin<P: GpuPlugin> {
pub(super) viewport: ViewportId,
pub(super) inner: P,
}
impl<P: GpuPlugin> GpuPlugin for ViewportScopedPlugin<P> {
fn priority(&self) -> i32 {
self.inner.priority()
}
fn init_gpu(&mut self, device: &wgpu::Device) {
self.inner.init_gpu(device);
}
fn on_device_recreated(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
self.inner.on_device_recreated(device, queue);
}
fn pre_prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
if ctx.viewport_id == Some(self.viewport) {
self.inner.pre_prepare(device, queue, ctx)
} else {
Vec::new()
}
}
fn post_paint(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
targets: &PostPaintTargets<'_>,
ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
if ctx.viewport_id == Some(self.viewport) {
self.inner.post_paint(device, queue, targets, ctx)
} else {
Vec::new()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct RecordingPlugin {
prio: i32,
log: Arc<Mutex<Vec<i32>>>,
post_log: Arc<Mutex<Vec<i32>>>,
init_count: Arc<Mutex<u32>>,
}
impl GpuPlugin for RecordingPlugin {
fn priority(&self) -> i32 {
self.prio
}
fn init_gpu(&mut self, _device: &wgpu::Device) {
*self.init_count.lock().unwrap() += 1;
}
fn pre_prepare(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
self.log.lock().unwrap().push(self.prio);
Vec::new()
}
fn post_paint(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_targets: &PostPaintTargets<'_>,
_ctx: &GpuFrameContext<'_>,
) -> Vec<wgpu::CommandBuffer> {
self.post_log.lock().unwrap().push(self.prio);
Vec::new()
}
}
fn make_dummy_view(
device: &wgpu::Device,
format: wgpu::TextureFormat,
usage: wgpu::TextureUsages,
) -> (wgpu::Texture, wgpu::TextureView) {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("post_paint_test_target"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
}
fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
#[test]
fn priority_order_and_init_once() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let log = Arc::new(Mutex::new(Vec::new()));
let post_log = Arc::new(Mutex::new(Vec::new()));
let init_a = Arc::new(Mutex::new(0));
let init_b = Arc::new(Mutex::new(0));
let mut runtime = crate::runtime::ViewportRuntime::new()
.with_gpu_plugin(RecordingPlugin {
prio: 200,
log: log.clone(),
post_log: post_log.clone(),
init_count: init_a.clone(),
})
.with_gpu_plugin(RecordingPlugin {
prio: 100,
log: log.clone(),
post_log: post_log.clone(),
init_count: init_b.clone(),
});
let camera = Camera::default();
let ctx = GpuFrameContext::new(&camera, glam::Vec2::new(800.0, 600.0), 1.0 / 60.0, 0);
let bufs = runtime.pre_prepare(&device, &queue, &ctx);
assert!(
bufs.is_empty(),
"empty plugin returns should concatenate cleanly"
);
assert_eq!(
*log.lock().unwrap(),
vec![100, 200],
"ascending priority order"
);
assert_eq!(*init_a.lock().unwrap(), 1);
assert_eq!(*init_b.lock().unwrap(), 1);
let _ = runtime.pre_prepare(&device, &queue, &ctx);
assert_eq!(*init_a.lock().unwrap(), 1);
assert_eq!(*init_b.lock().unwrap(), 1);
assert_eq!(*log.lock().unwrap(), vec![100, 200, 100, 200]);
let (_color_tex, color_view) = make_dummy_view(
&device,
wgpu::TextureFormat::Rgba8UnormSrgb,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
);
let (_depth_tex, depth_view) = make_dummy_view(
&device,
wgpu::TextureFormat::Depth32Float,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
);
let targets = PostPaintTargets::new(
&color_view,
&depth_view,
wgpu::TextureFormat::Rgba8UnormSrgb,
);
let bufs = runtime.post_paint(&device, &queue, &targets, &ctx);
assert!(bufs.is_empty());
assert_eq!(*post_log.lock().unwrap(), vec![100, 200]);
}
struct DeviceLossRecorder {
recreated: Arc<Mutex<u32>>,
init_count: Arc<Mutex<u32>>,
}
impl GpuPlugin for DeviceLossRecorder {
fn init_gpu(&mut self, _device: &wgpu::Device) {
*self.init_count.lock().unwrap() += 1;
}
fn on_device_recreated(&mut self, _device: &wgpu::Device, _queue: &wgpu::Queue) {
*self.recreated.lock().unwrap() += 1;
}
}
#[test]
fn notify_device_recreated_invokes_hook_and_reinits() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let recreated = Arc::new(Mutex::new(0));
let init_count = Arc::new(Mutex::new(0));
let mut runtime =
crate::runtime::ViewportRuntime::new().with_gpu_plugin(DeviceLossRecorder {
recreated: recreated.clone(),
init_count: init_count.clone(),
});
let camera = Camera::default();
let ctx = GpuFrameContext::new(&camera, glam::Vec2::new(1.0, 1.0), 0.0, 0);
let _ = runtime.pre_prepare(&device, &queue, &ctx);
assert_eq!(*init_count.lock().unwrap(), 1);
assert_eq!(*recreated.lock().unwrap(), 0);
runtime.notify_device_recreated(&device, &queue);
assert_eq!(*recreated.lock().unwrap(), 1);
let _ = runtime.pre_prepare(&device, &queue, &ctx);
assert_eq!(*init_count.lock().unwrap(), 2);
}
#[test]
fn viewport_scoped_plugin_only_runs_for_matching_viewport() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let log = Arc::new(Mutex::new(Vec::new()));
let post_log = Arc::new(Mutex::new(Vec::new()));
let init_count = Arc::new(Mutex::new(0));
let vp_a = crate::renderer::ViewportId(0);
let vp_b = crate::renderer::ViewportId(1);
let mut runtime = crate::runtime::ViewportRuntime::new().with_gpu_plugin_for_viewport(
vp_a,
RecordingPlugin {
prio: 100,
log: log.clone(),
post_log: post_log.clone(),
init_count: init_count.clone(),
},
);
let camera = Camera::default();
let sz = glam::Vec2::new(1.0, 1.0);
let ctx_a = GpuFrameContext::new(&camera, sz, 0.0, 0).with_viewport(vp_a);
let ctx_b = GpuFrameContext::new(&camera, sz, 0.0, 0).with_viewport(vp_b);
let ctx_none = GpuFrameContext::new(&camera, sz, 0.0, 0);
let _ = runtime.pre_prepare(&device, &queue, &ctx_a);
let _ = runtime.pre_prepare(&device, &queue, &ctx_b);
let _ = runtime.pre_prepare(&device, &queue, &ctx_none);
assert_eq!(*log.lock().unwrap(), vec![100], "only ran for vp_a");
assert_eq!(*init_count.lock().unwrap(), 1);
}
}