use crate::graphics::core::Graphics;
use crate::window::{SceneHost, make_screen_target};
use crate::{Context, Spot};
use std::pin::Pin;
use std::time::Duration;
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub struct SpotRendererOptions {
pub target_format: wgpu::TextureFormat,
pub initial_size: [u32; 2],
pub scale_factor: f64,
pub transparent: bool,
}
impl Default for SpotRendererOptions {
fn default() -> Self {
Self {
target_format: wgpu::TextureFormat::Rgba8Unorm,
initial_size: [1, 1],
scale_factor: 1.0,
transparent: false,
}
}
}
impl SpotRendererOptions {
fn validate(self) -> anyhow::Result<Self> {
if self.initial_size[0] == 0 || self.initial_size[1] == 0 {
anyhow::bail!(
"initial render target dimensions must be non-zero, got {}x{}",
self.initial_size[0],
self.initial_size[1]
);
}
if !self.scale_factor.is_finite() || self.scale_factor <= 0.0 {
anyhow::bail!(
"scale factor must be finite and positive, got {}",
self.scale_factor
);
}
Ok(self)
}
}
#[doc(hidden)]
pub struct SpotRenderer {
ctx: Pin<Box<Context>>,
scene: SceneHost,
scale_factor: f64,
removed: bool,
}
impl SpotRenderer {
pub fn from_wgpu<T: Spot + 'static>(
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
options: SpotRendererOptions,
) -> anyhow::Result<Self> {
let options = options.validate()?;
let mut ctx = Box::pin(Context::new());
ctx.update_window_metrics_physical(
options.initial_size[0],
options.initial_size[1],
options.scale_factor,
);
ctx.attach_graphics(Graphics::from_wgpu(
adapter,
device,
queue,
options.target_format,
options.initial_size[0],
options.initial_size[1],
options.transparent,
));
let mut scene = SceneHost::new::<T>();
scene.initialize_if_missing(&mut ctx);
Ok(Self {
ctx,
scene,
scale_factor: options.scale_factor,
removed: false,
})
}
pub fn update(&mut self, dt: Duration) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx.set_delta_time(dt);
if let Some(spot) = self.scene.spot_mut() {
spot.update(&mut self.ctx, dt);
}
self.ctx.input_mut().end_frame();
Ok(())
}
pub fn render_to_view(
&mut self,
target: &wgpu::TextureView,
size: [u32; 2],
) -> anyhow::Result<()> {
self.ensure_active()?;
if size[0] == 0 || size[1] == 0 {
anyhow::bail!(
"render target dimensions must be non-zero, got {}x{}",
size[0],
size[1]
);
}
self.ctx
.update_window_metrics_physical(size[0], size[1], self.scale_factor);
self.ctx.begin_frame();
let screen = make_screen_target(&self.ctx);
if let Some(spot) = self.scene.spot_mut() {
spot.draw(&mut self.ctx, screen);
}
self.scene.apply_pending_switch(&mut self.ctx);
let mut graphics = self
.ctx
.detach_graphics()
.ok_or_else(|| anyhow::anyhow!("Spot renderer GPU state is not attached"))?;
let result = graphics.render_to_view(&mut self.ctx, target, size);
self.ctx.attach_graphics(graphics);
result
}
pub fn set_scale_factor(&mut self, scale_factor: f64) -> anyhow::Result<()> {
if !scale_factor.is_finite() || scale_factor <= 0.0 {
anyhow::bail!(
"scale factor must be finite and positive, got {}",
scale_factor
);
}
self.scale_factor = scale_factor;
Ok(())
}
pub fn set_draw_interpolation(&mut self, alpha: f32) -> anyhow::Result<()> {
self.ensure_active()?;
if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
anyhow::bail!("draw interpolation must be between 0 and 1, got {alpha}");
}
self.ctx.set_draw_alpha(alpha);
Ok(())
}
pub fn set_input_focus(&mut self, focused: bool) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx.input_mut().handle_focus(focused);
Ok(())
}
pub fn set_cursor_position(&mut self, position: [f32; 2]) -> anyhow::Result<()> {
self.ensure_active()?;
if !position[0].is_finite() || !position[1].is_finite() {
anyhow::bail!(
"cursor position must be finite, got [{}, {}]",
position[0],
position[1]
);
}
self.ctx
.input_mut()
.handle_cursor_moved(crate::Pt::from(position[0]), crate::Pt::from(position[1]));
Ok(())
}
#[cfg(not(target_os = "android"))]
pub fn handle_mouse_input(
&mut self,
state: winit::event::ElementState,
button: winit::event::MouseButton,
) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx.input_mut().handle_mouse_input(state, button);
Ok(())
}
#[cfg(not(target_os = "android"))]
pub fn handle_mouse_wheel(
&mut self,
delta: winit::event::MouseScrollDelta,
) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx.input_mut().handle_mouse_wheel(delta);
Ok(())
}
#[cfg(not(target_os = "android"))]
pub fn handle_keyboard_input(
&mut self,
state: winit::event::ElementState,
physical_key: winit::keyboard::PhysicalKey,
) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx
.input_mut()
.handle_keyboard_input(state, physical_key);
Ok(())
}
pub fn end_input_frame(&mut self) -> anyhow::Result<()> {
self.ensure_active()?;
self.ctx.input_mut().end_frame();
Ok(())
}
pub fn resumed(&mut self) -> anyhow::Result<()> {
self.ensure_active()?;
if let Some(spot) = self.scene.spot_mut() {
spot.resumed(&mut self.ctx);
}
Ok(())
}
pub fn suspended(&mut self) -> anyhow::Result<()> {
self.ensure_active()?;
if let Some(spot) = self.scene.spot_mut() {
spot.suspended(&mut self.ctx);
}
self.ctx.clear_transient_input();
self.ctx.clear_transient_state();
Ok(())
}
pub fn remove(&mut self) {
if self.removed {
return;
}
self.scene.remove_current(&mut self.ctx);
if let Some(graphics) = self.ctx.graphics_mut() {
graphics.finish_profiling();
}
self.removed = true;
}
fn ensure_active(&self) -> anyhow::Result<()> {
if self.removed {
anyhow::bail!("Spot renderer has been removed");
}
Ok(())
}
}
impl Drop for SpotRenderer {
fn drop(&mut self) {
self.remove();
}
}
#[cfg(test)]
mod tests {
use super::{SpotRenderer, SpotRendererOptions};
use crate::{Context, Image, Spot};
use std::time::Duration;
struct EmptyScene;
impl Spot for EmptyScene {
fn initialize(_ctx: &mut Context) -> Self {
Self
}
fn draw(&mut self, _ctx: &mut Context, _screen: Image) {}
}
#[test]
fn default_options_match_editor_viewport_format() {
let options = SpotRendererOptions::default();
assert_eq!(options.target_format, wgpu::TextureFormat::Rgba8Unorm);
assert_eq!(options.initial_size, [1, 1]);
assert_eq!(options.scale_factor, 1.0);
assert!(!options.transparent);
}
#[test]
fn options_reject_invalid_dimensions_and_scale() {
assert!(
SpotRendererOptions {
initial_size: [0, 1],
..Default::default()
}
.validate()
.is_err()
);
assert!(
SpotRendererOptions {
scale_factor: 0.0,
..Default::default()
}
.validate()
.is_err()
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn shared_gpu_renderer_renders_to_external_view() {
let instance = wgpu::Instance::default();
let Ok(adapter) =
crate::platform::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
else {
eprintln!("skipping shared GPU test because no adapter is available");
return;
};
let (device, queue) =
crate::platform::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("spot_shared_gpu_test_device"),
required_features: wgpu::Features::empty(),
required_limits: adapter.limits(),
experimental_features: wgpu::ExperimentalFeatures::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
}))
.expect("request test device");
let poll_device = device.clone();
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("spot_shared_gpu_test_target"),
size: wgpu::Extent3d {
width: 8,
height: 8,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = SpotRenderer::from_wgpu::<EmptyScene>(
adapter,
device,
queue,
SpotRendererOptions {
initial_size: [8, 8],
..Default::default()
},
)
.expect("construct shared GPU renderer");
renderer
.render_to_view(&view, [8, 8])
.expect("render to external view");
poll_device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(Duration::from_secs(5)),
})
.expect("wait for shared GPU render");
}
}