euv_engine/raytracing/backend/data/impl.rs
1use super::*;
2
3/// Implements construction, scene updates, rendering, resizing, and
4/// teardown for [`RayTraceRenderer`].
5///
6/// Only [`RayTraceRenderer::init`] is asynchronous, because it is the
7/// only step that awaits a browser promise. Every other call is
8/// synchronous, so a render loop drives the renderer without awaiting.
9///
10/// Every step that can fail validates everything it can before it
11/// mutates anything, and commits its own state change only after the
12/// step that can fail has succeeded. A rejected call therefore leaves
13/// the renderer exactly as it was, on the canvas as well as in memory.
14impl RayTraceRenderer {
15 /// Creates a renderer bound to one explicitly requested backend.
16 ///
17 /// The requested surface dimensions and the initial scene are both
18 /// validated before any GPU resource is created, so an illegal
19 /// input is rejected before a device, a pipeline, or two storage
20 /// buffers are allocated and then thrown away.
21 ///
22 /// The backend is never substituted. If the requested backend is
23 /// unavailable, if a shader fails to compile, or if the WebGPU
24 /// pipeline fails validation, this returns the driver's own
25 /// diagnostic — awaiting the WebGPU compilation info and error
26 /// scopes, because WebGPU reports both asynchronously and a
27 /// `catchJs` wrapper around the synchronous calls would report
28 /// success for an invalid pipeline.
29 ///
30 /// # Arguments
31 ///
32 /// - `HtmlCanvasElement` - The canvas to draw into.
33 /// - `RayTraceBackend` - The backend to drive, with no fallback.
34 /// - `&RayTraceScene` - The initial scene, packed and uploaded here.
35 ///
36 /// # Returns
37 ///
38 /// - `Result<Self, JsValue>` - The ready renderer, or the reason it
39 /// could not be created.
40 pub async fn init(
41 canvas: HtmlCanvasElement,
42 backend: RayTraceBackend,
43 scene: &RayTraceScene,
44 ) -> Result<Self, JsValue> {
45 let width: u32 = canvas.width();
46 let height: u32 = canvas.height();
47 validate_surface(width, height, None)?;
48 if let Err(reason) = scene.encode_gpu() {
49 return Err(backend_error(ERROR_SCENE_PACK, reason));
50 }
51 let mut renderer: RayTraceRenderer = RayTraceRenderer {
52 canvas,
53 backend,
54 adapter: None,
55 scene: RayTraceScene::new(Vec::new()),
56 packed_frame: Vec::new(),
57 pixels: Vec::new(),
58 width,
59 height,
60 disposed: false,
61 };
62 // The adapter owns the GPU resources, so a failure after this
63 // point has to run its release. `adapter` is filled in before
64 // the fallible steps below precisely so that the early return
65 // releases it instead of leaking it.
66 renderer.set_adapter(Some(
67 RayTraceBackendAdapter::attach(renderer.get_canvas(), backend, width, height).await?,
68 ));
69 if let Err(reason) = renderer.resize(width, height) {
70 renderer.release_backend();
71 return Err(reason);
72 }
73 if let Err(reason) = renderer.update_scene(scene) {
74 renderer.release_backend();
75 return Err(reason);
76 }
77 Ok(renderer)
78 }
79
80 /// Replaces the scene, re-packing and re-uploading it exactly once.
81 ///
82 /// The update is transactional. The new scene is packed into a
83 /// buffer staged outside the renderer, then uploaded; only once the
84 /// upload has succeeded is the renderer's own scene mirror swapped
85 /// in. A failed upload therefore leaves the previous scene both in
86 /// the renderer and, because the adapter only rebinds a texture
87 /// after the upload is accepted, still bound on the GPU. The
88 /// staged block is the only allocation of the call.
89 ///
90 /// # Arguments
91 ///
92 /// - `&RayTraceScene` - The new scene.
93 ///
94 /// # Returns
95 ///
96 /// - `Result<(), JsValue>` - `Ok` once the new scene is live.
97 pub fn update_scene(&mut self, scene: &RayTraceScene) -> Result<(), JsValue> {
98 if self.get_disposed() {
99 return Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string()));
100 }
101 let staged: Vec<f32> = match scene.encode_gpu() {
102 Ok(packed) => packed,
103 Err(reason) => return Err(backend_error(ERROR_SCENE_PACK, reason)),
104 };
105 let outcome: Result<(), JsValue> = match self.get_mut_adapter().as_mut() {
106 Some(adapter) => adapter.upload_scene(&staged),
107 None => Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string())),
108 };
109 outcome?;
110 self.set_scene(scene.clone());
111 Ok(())
112 }
113
114 /// Traces one frame with the current scene at the given camera.
115 ///
116 /// A steady-state frame reuses every buffer it already owns. The
117 /// Canvas path moves the pixel buffer out, traces into it, and moves
118 /// it back whether the trace succeeded or failed, so a rejected
119 /// frame still leaves the buffer allocated for the next attempt.
120 /// The GPU paths refill the packed frame through its existing
121 /// capacity and issue one draw.
122 ///
123 /// # Arguments
124 ///
125 /// - `&Camera3D` - The camera supplying the eye, target, up, and
126 /// field of view.
127 /// - `&LightingUniforms` - The lights, ambient, and shading eye.
128 /// - `&RayTraceSettings` - The sampling grid and bounce limit.
129 ///
130 /// # Returns
131 ///
132 /// - `Result<(), JsValue>` - `Ok` once the frame is submitted.
133 pub fn render(
134 &mut self,
135 camera: &Camera3D,
136 lights: &LightingUniforms,
137 settings: &RayTraceSettings,
138 ) -> Result<(), JsValue> {
139 if self.get_disposed() {
140 return Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string()));
141 }
142 match self.get_backend() {
143 RayTraceBackend::Canvas2D => self.render_canvas(camera, lights, settings),
144 RayTraceBackend::WebGl2 | RayTraceBackend::WebGpu => {
145 self.render_gpu(camera, lights, settings)
146 }
147 }
148 }
149
150 /// Traces the CPU frame and blits it through the reused image.
151 ///
152 /// # Arguments
153 ///
154 /// - `&Camera3D` - The camera supplying the eye, target, up, and
155 /// field of view.
156 /// - `&LightingUniforms` - The lights, ambient, and shading eye.
157 /// - `&RayTraceSettings` - The sampling grid and bounce limit.
158 ///
159 /// # Returns
160 ///
161 /// - `Result<(), JsValue>` - `Ok` once the frame is blitted.
162 fn render_canvas(
163 &mut self,
164 camera: &Camera3D,
165 lights: &LightingUniforms,
166 settings: &RayTraceSettings,
167 ) -> Result<(), JsValue> {
168 let mut pixels: Vec<u8> = mem::take(self.get_mut_pixels());
169 let frame: RayTraceFrame<'_> = RayTraceFrame {
170 camera,
171 lights,
172 settings,
173 width: self.get_width(),
174 height: self.get_height(),
175 };
176 let traced: Result<(), String> =
177 render_raytrace_rgba(self.get_scene(), &frame, &mut pixels);
178 let presented: Result<(), JsValue> = match self.get_mut_adapter().as_mut() {
179 Some(adapter) => match traced {
180 Ok(()) => adapter.present(&pixels),
181 Err(reason) => Err(backend_error(ERROR_FRAME_PACK, reason)),
182 },
183 None => Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string())),
184 };
185 *self.get_mut_pixels() = pixels;
186 presented
187 }
188
189 /// Refills the packed frame and issues the GPU draw.
190 ///
191 /// # Arguments
192 ///
193 /// - `&Camera3D` - The camera supplying the eye, target, up, and
194 /// field of view.
195 /// - `&LightingUniforms` - The lights, ambient, and shading eye.
196 /// - `&RayTraceSettings` - The sampling grid and bounce limit.
197 ///
198 /// # Returns
199 ///
200 /// - `Result<(), JsValue>` - `Ok` once the frame is submitted.
201 fn render_gpu(
202 &mut self,
203 camera: &Camera3D,
204 lights: &LightingUniforms,
205 settings: &RayTraceSettings,
206 ) -> Result<(), JsValue> {
207 let mut frame: Vec<f32> = mem::take(self.get_mut_packed_frame());
208 let inputs: RayTraceFrame<'_> = RayTraceFrame {
209 camera,
210 lights,
211 settings,
212 width: self.get_width(),
213 height: self.get_height(),
214 };
215 let packed: Result<(), String> = pack_raytrace_frame(&mut frame, &inputs);
216 let drawn: Result<(), JsValue> = match (packed, self.get_mut_adapter().as_mut()) {
217 (Ok(()), Some(adapter)) => adapter.draw(&frame),
218 (Ok(()), None) => Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string())),
219 (Err(reason), _) => Err(backend_error(ERROR_FRAME_PACK, reason)),
220 };
221 *self.get_mut_packed_frame() = frame;
222 drawn
223 }
224
225 /// Resizes the surface, reallocating only what the new size needs.
226 ///
227 /// The call is transactional in both directions. The requested size
228 /// is validated first, then the adapter applies it, because the
229 /// adapter is the only step that can fail for a reason the shared
230 /// layer cannot check: a lost context or a lost device. Only once
231 /// that step has succeeded are the renderer's own size, the canvas
232 /// backing store, and the CPU pixel buffer updated — so a rejected
233 /// request leaves the canvas showing the previous, still valid,
234 /// surface instead of one the renderer never adopted. The order
235 /// within the commit does not matter, because nothing past the
236 /// adapter can fail.
237 ///
238 /// A zero, overflowing, or oversized request is rejected before any
239 /// state is touched. A size the hardware cannot address is an error
240 /// rather than a silent clamp.
241 ///
242 /// # Arguments
243 ///
244 /// - `u32` - The new width in pixels.
245 /// - `u32` - The new height in pixels.
246 ///
247 /// # Returns
248 ///
249 /// - `Result<(), JsValue>` - `Ok` once the new size is live.
250 pub fn resize(&mut self, width: u32, height: u32) -> Result<(), JsValue> {
251 if self.get_disposed() {
252 return Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string()));
253 }
254 let byte_length: usize = match self.get_mut_adapter().as_mut() {
255 Some(adapter) => {
256 let limit: Option<u32> = adapter.dimension_limit();
257 let bytes: usize = validate_surface(width, height, limit)?;
258 adapter.resize(width, height)?;
259 bytes
260 }
261 None => {
262 validate_surface(width, height, None)?;
263 return Err(backend_error(ERROR_DISPOSED, ERROR_DISPOSED.to_string()));
264 }
265 };
266 self.set_width(width);
267 self.set_height(height);
268 self.get_mut_canvas().set_width(width);
269 self.get_mut_canvas().set_height(height);
270 resize_pixel_buffer(self.get_mut_pixels(), byte_length);
271 Ok(())
272 }
273
274 /// Releases every resource the renderer owns, idempotently.
275 ///
276 /// Calling this twice is safe: the second call finds the disposed
277 /// flag already set and releases nothing. Cleanup also runs when the
278 /// renderer is dropped, and the flag is set before the release so a
279 /// renderer dropped right after an explicit dispose destroys nothing
280 /// twice.
281 pub fn dispose(&mut self) {
282 if self.get_disposed() {
283 return;
284 }
285 self.set_disposed(true);
286 self.release_backend();
287 }
288
289 /// Destroys the adapter's resources at most once.
290 pub(crate) fn release_backend(&mut self) {
291 if let Some(mut adapter) = mem::take(self.get_mut_adapter()) {
292 adapter.release();
293 }
294 }
295}
296
297impl Drop for RayTraceRenderer {
298 /// Releases the backend resources the renderer owns.
299 ///
300 /// Cleanup is idempotent: a renderer that was explicitly disposed
301 /// releases nothing here, and one dropped without being disposed
302 /// releases exactly what it still owns, once.
303 fn drop(&mut self) {
304 if self.get_disposed() {
305 return;
306 }
307 self.set_disposed(true);
308 self.release_backend();
309 }
310}