1use std::sync::Arc;
2
3use spatialrust_gpu::{WgpuPowerPreference, WgpuRuntime};
4use spatialrust_render_wgpu::{GpuGeometry, HeadlessRender, RenderOptions, WgpuRenderer};
5use spatialrust_viz::{LinearRgba, VisualStyle};
6
7use crate::{WebError, WebResult, WebViewerState};
8
9pub struct WebGpuFrame {
11 pub state_revision: u64,
13 pub render: HeadlessRender,
15}
16
17pub struct WebGpuViewer {
24 state: WebViewerState,
25 renderer: WgpuRenderer,
26}
27
28impl WebGpuViewer {
29 pub async fn new_async(
34 state: WebViewerState,
35 preference: WgpuPowerPreference,
36 ) -> WebResult<Self> {
37 let runtime = WgpuRuntime::new_headless_async(preference)
38 .await
39 .map_err(|error| WebError::WebGpu(error.to_string()))?;
40 Self::try_new(state, Arc::new(runtime))
41 }
42
43 pub fn try_new(state: WebViewerState, runtime: Arc<WgpuRuntime>) -> WebResult<Self> {
45 state.validate()?;
46 Ok(Self { state, renderer: WgpuRenderer::new(runtime) })
47 }
48
49 #[must_use]
51 pub const fn state(&self) -> &WebViewerState {
52 &self.state
53 }
54
55 pub fn set_state(&mut self, state: WebViewerState) -> WebResult<()> {
57 state.validate()?;
58 self.state = state;
59 Ok(())
60 }
61
62 #[must_use]
64 pub const fn renderer(&self) -> &WgpuRenderer {
65 &self.renderer
66 }
67
68 pub fn render(
70 &self,
71 geometry: &GpuGeometry,
72 style: VisualStyle,
73 clear_color: LinearRgba,
74 ) -> WebResult<WebGpuFrame> {
75 let options = RenderOptions::try_new(
76 self.state.viewer.viewport.width,
77 self.state.viewer.viewport.height,
78 self.state.viewer.camera,
79 style,
80 clear_color,
81 )
82 .map_err(|error| WebError::WebGpu(error.to_string()))?;
83 let render = self
84 .renderer
85 .render_headless(geometry, &options)
86 .map_err(|error| WebError::WebGpu(error.to_string()))?;
87 Ok(WebGpuFrame { state_revision: self.state.revision, render })
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use std::sync::Arc;
94
95 use spatialrust_gpu::WgpuRuntime;
96 use spatialrust_math::Vec3;
97 use spatialrust_viewer::{ViewerState, ViewportSize};
98 use spatialrust_viz::{
99 Camera, LinearRgba, PointCloudView, PointColor, PointStyle, PositionColumns3, Projection,
100 VisualPrimitive, VisualStyle,
101 };
102
103 use crate::WebViewerState;
104
105 #[test]
106 fn web_bridge_matches_shared_headless_renderer_pixels_and_revision() {
107 let runtime = Arc::new(WgpuRuntime::new_headless().unwrap());
108 let camera = Camera::try_new(
109 Vec3::new(0.0, 0.0, 5.0),
110 Vec3::new(0.0, 0.0, 0.0),
111 Vec3::new(0.0, 1.0, 0.0),
112 Projection::Perspective { vertical_fov_radians: 1.0, near: 0.1, far: 100.0 },
113 )
114 .unwrap();
115 let mut state = WebViewerState::try_new(
116 ViewerState::try_new(camera, ViewportSize::try_new(64, 64).unwrap()).unwrap(),
117 )
118 .unwrap();
119 state.revision = 7;
120 let viewer = super::WebGpuViewer::try_new(state, runtime).unwrap();
121 let x = [0.0];
122 let y = [0.0];
123 let z = [0.0];
124 let points = PointCloudView::positions_only(PositionColumns3::try_new(&x, &y, &z).unwrap());
125 let (geometry, upload) = viewer.renderer().upload(VisualPrimitive::Points(points)).unwrap();
126 assert_eq!(upload.total_bytes().unwrap(), 12);
127 let style = VisualStyle::Points(
128 PointStyle::try_new(
129 8.0,
130 PointColor::Uniform(LinearRgba::try_new(1.0, 0.0, 0.0, 1.0).unwrap()),
131 )
132 .unwrap(),
133 );
134 let frame = viewer.render(&geometry, style, LinearRgba::BLACK).unwrap();
135 assert_eq!(frame.state_revision, 7);
136 let image = viewer.renderer().readback_rgba(&frame.render.target).unwrap();
137 let center = ((32 * 64 + 32) * 4) as usize;
138 assert!(image.rgba[center] > 200);
139 assert!(image.rgba[center + 1] < 20);
140 assert_eq!(image.rgba.len(), 64 * 64 * 4);
141 }
142}