Skip to main content

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}