Skip to main content

roxlap_render/
lib.rs

1//! roxlap-render — unified CPU/GPU renderer facade.
2//!
3//! One [`SceneRenderer`] hides the choice between the CPU opticast
4//! path (`roxlap-core` / `roxlap-scene`, presented via `softbuffer`)
5//! and the GPU compute-shader path (`roxlap-gpu`, presented via its
6//! own wgpu surface). Construction picks the GPU backend when asked
7//! and able, and **falls back to CPU automatically** when WGPU init
8//! fails — so a host never has to branch on GPU availability or carry
9//! the `Scene`→GPU upload/refresh/transform glue itself.
10//!
11//! Hosts stay thin: build a `Scene`, advance it from input, then call
12//! [`SceneRenderer::render`] each frame. The facade owns the window
13//! surface, the framebuffer/z-buffer (CPU) or the resident scene +
14//! dirty-chunk tracking (GPU), and presentation.
15//!
16//! The per-frame flow is `render` → *(optional overlays)* → finish.
17//! Between [`SceneRenderer::render`] and the finishing
18//! [`SceneRenderer::present`] / [`SceneRenderer::paint_egui`] call, a
19//! host may overlay depth-tested world-space lines with
20//! [`SceneRenderer::draw_lines`] (editor gizmos, debug geometry — see
21//! [`Line3`]); they land in the framebuffer, occluded by the rendered
22//! scene, with egui still painting panels on top.
23//!
24//! This is the RF.0 skeleton: backend selection + fallback + a
25//! clear-to-sky frame. RF.1/RF.2 fill in the real CPU/GPU scene
26//! render; RF.3 adds sprites; RF.4 adds framebuffer capture.
27
28#![forbid(unsafe_code)]
29
30mod cpu;
31/// WebGL2 framebuffer presenter for the CPU backend on wasm (the
32/// browser has no `softbuffer`).
33#[cfg(target_arch = "wasm32")]
34mod cpu_blit;
35#[cfg(feature = "hud")]
36mod cpu_egui;
37mod gpu;
38
39#[cfg(not(target_arch = "wasm32"))]
40use std::sync::Arc;
41
42use roxlap_core::opticast::OpticastSettings;
43use roxlap_core::sky::Sky;
44use roxlap_core::sprite::SpriteLighting;
45use roxlap_core::Camera;
46use roxlap_scene::Scene;
47
48pub use roxlap_formats::kfa::KfaSprite;
49pub use roxlap_formats::kv6::Kv6;
50pub use roxlap_formats::sprite::Sprite;
51pub use roxlap_gpu::{GpuInitError, GpuRendererSettings, PowerPreference};
52// Re-exported so hosts can name the [`SceneRenderer::new`] bounds
53// without adding a direct `raw-window-handle` dependency of their own.
54pub use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
55// Re-exported so hosts feed [`SceneRenderer::paint_egui`] from the exact
56// egui version the renderer was built against (`hud` feature).
57#[cfg(feature = "hud")]
58pub use egui;
59
60use crate::cpu::CpuBackend;
61use crate::gpu::GpuBackend;
62
63/// Type-erased display handle stored by the CPU backend's softbuffer
64/// surface. `raw-window-handle` implements `HasDisplayHandle` for
65/// `Arc<H>` (`H: ?Sized`), and the bare trait object implements its
66/// own object-safe trait — so `Arc<W>` coerces to `Arc<DynDisplay>`
67/// for any provider `W`.
68#[cfg(not(target_arch = "wasm32"))]
69pub(crate) type DynDisplay = dyn HasDisplayHandle + Send + Sync + 'static;
70/// Type-erased window handle counterpart to [`DynDisplay`].
71#[cfg(not(target_arch = "wasm32"))]
72pub(crate) type DynWindow = dyn HasWindowHandle + Send + Sync + 'static;
73
74/// One placed sprite instance: which [`SpriteSet::models`] entry and
75/// where in the world.
76pub struct SpriteInstanceDesc {
77    pub model: usize,
78    pub pos: [f32; 3],
79}
80
81/// Stable handle to a registered sprite model, returned (one per
82/// [`SpriteSet::models`] entry, in order) by
83/// [`SceneRenderer::set_sprites`]. Pass it to
84/// [`refresh_sprite_model`](SceneRenderer::refresh_sprite_model) to
85/// re-register that model's geometry after a content edit — so callers
86/// never track the positional `usize` index themselves. Opaque on
87/// purpose: there is no arithmetic to do on it.
88#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
89pub struct SpriteModelId(pub(crate) usize);
90
91/// Backend-agnostic sprite description. The facade builds the CPU
92/// per-instance draw list and the GPU instanced registry from the
93/// same data, so both backends show identical sprites. The host owns
94/// content (which models, where, recolouring) — building a recoloured
95/// variant is just a second [`Sprite`] model with edited `kv6.voxels`.
96pub struct SpriteSet {
97    /// Distinct voxel models (KV6 + base orientation). Instances index
98    /// into this; their position overrides the model's.
99    pub models: Vec<Sprite>,
100    pub instances: Vec<SpriteInstanceDesc>,
101    /// Model the [`SceneRenderer::carve_active_sprite`] hotkey edits
102    /// (GPU only, mirroring the demo's `G`-carve). `None` disables it.
103    pub carve_model: Option<usize>,
104}
105
106/// Per-frame inputs both backends consume. The host builds the
107/// [`OpticastSettings`] (it owns scan distance etc.); the facade does
108/// everything else (pool config, sky fill, render, present).
109pub struct FrameParams<'a> {
110    /// CPU opticast settings (scan distance, mip ladder, framebuffer
111    /// geometry). Ignored by the GPU backend.
112    pub settings: &'a OpticastSettings,
113    /// Packed engine sky colour: the CPU sky-miss fill + skycast, and
114    /// the clear colour if no scene renders.
115    pub sky_color: u32,
116    /// Optional sky panorama for the CPU rasterizer's sky sampling.
117    pub sky: Option<&'a Sky>,
118    /// CPU fog: packed colour + max scan distance (voxels). `0` scan
119    /// distance disables CPU fog.
120    pub fog_color: u32,
121    pub fog_max_scan_dist: i32,
122    /// CPU: treat z=255 as air (avoids the S1.X bedrock path for
123    /// out-of-bounds cameras).
124    pub treat_z_max_as_air: bool,
125    /// GPU scene-grid LOD scan distance (world units); see GPU.11.1.
126    /// Ignored by the CPU backend.
127    pub gpu_mip_scan_dist: f32,
128    /// GPU outer-DDA step budget (chunks). Ignored by the CPU backend.
129    pub gpu_max_outer_steps: u32,
130    /// GPU vertical field of view (radians). Ignored by the CPU
131    /// backend (it derives projection from [`OpticastSettings`]).
132    pub gpu_fov_y_rad: f32,
133    /// CPU sprite shading (built by the host from its engine). Required
134    /// for the CPU backend to draw sprites; ignored by the GPU backend
135    /// (its sprite pass shades from the uploaded model colours). `None`
136    /// skips CPU sprite drawing.
137    pub sprite_lighting: Option<&'a SpriteLighting<'a>>,
138    /// Per-face directional shading for the voxel grids — voxlap's
139    /// `setsideshades(top, bot, left, right, up, down)`, the grid-scan
140    /// analogue of [`sprite_lighting`](Self::sprite_lighting). Each
141    /// entry darkens the faces pointing that way; the host typically
142    /// passes its engine's `side_shades()`. The default `[0; 6]` keeps
143    /// `sideshademode` off (no per-side shading), so existing hosts and
144    /// the oracle goldens are unaffected. Applied each frame by **both**
145    /// backends: the CPU rasteriser via `gcsub`, and the GPU scene-DDA
146    /// pass by darkening a hit voxel's brightness by the hit face's
147    /// shade (the face taken from the DDA's last-stepped axis).
148    pub side_shades: [i8; 6],
149}
150
151/// Result of [`SceneRenderer::pick`] — a resolved screen→world voxel
152/// hit. `world` is the surface point (`cam.pos + t · normalize(ray)`);
153/// `grid` + `voxel` are the owning grid and its **grid-local** voxel
154/// (transform-correct for rotated / translated grids).
155#[derive(Clone, Copy, PartialEq, Debug)]
156pub struct PickHit {
157    pub world: [f32; 3],
158    pub grid: roxlap_scene::GridId,
159    pub voxel: glam::IVec3,
160}
161
162/// A world-space view ray: the canonical unproject output of
163/// [`SceneRenderer::view_ray`]. `dir` is unit-length. Feed it straight
164/// to [`roxlap_scene::Scene::raycast`] for depth-free, backend-agnostic
165/// voxel picking (`scene.raycast(ray.origin, ray.dir, max_dist)`), or
166/// intersect it with a plane for tile selection.
167#[derive(Clone, Copy, PartialEq, Debug)]
168pub struct Ray {
169    pub origin: glam::DVec3,
170    pub dir: glam::DVec3,
171}
172
173/// A world-space line segment to draw over a rendered frame via
174/// [`SceneRenderer::draw_lines`] — editor gizmos (bounding boxes, floor
175/// grids, axes, hover wireframes), debug paths, etc.
176#[derive(Clone, Copy, PartialEq, Debug)]
177pub struct Line3 {
178    /// World-space endpoints (voxel units), in the same frame the
179    /// rendered scene + `camera` use.
180    pub a: [f64; 3],
181    pub b: [f64; 3],
182    /// `0xAARRGGBB` — the high byte is an alpha blend factor (`0xFF`
183    /// opaque, `0x00` invisible), the low 24 bits the RGB colour.
184    pub color: u32,
185    /// Screen-space thickness in pixels (`<= 1.0` draws a 1px line).
186    pub width_px: f32,
187    /// `true`: the segment is occluded by nearer rendered geometry
188    /// (depth-tested against the frame's z-buffer). `false`: always on
189    /// top (e.g. a hover highlight that should show through the model).
190    pub depth_test: bool,
191}
192
193/// Which renderer a [`SceneRenderer`] resolved to at construction.
194#[derive(Clone, Copy, PartialEq, Eq, Debug)]
195pub enum Backend {
196    /// `roxlap-core` opticast, presented via `softbuffer`.
197    Cpu,
198    /// `roxlap-gpu` compute marcher, presented via wgpu.
199    Gpu,
200}
201
202/// Construction-time options for [`SceneRenderer::new`].
203pub struct RenderOptions {
204    /// Try the GPU backend first. When `false`, or when GPU init
205    /// fails, the renderer uses the CPU backend.
206    pub want_gpu: bool,
207    /// Settings forwarded to [`roxlap_gpu::GpuRenderer`] when the GPU
208    /// backend is selected.
209    pub gpu: GpuRendererSettings,
210    /// Packed `0x00RRGGBB` (alpha ignored) the empty/clear frame fills
211    /// with until a scene render lands. Also the CPU sky-miss colour
212    /// default if a frame supplies none.
213    pub clear_sky: u32,
214    /// CPU [`ScratchPool`](roxlap_core::rasterizer::ScratchPool) `lastx`
215    /// sizing — the largest combined grid `vsid` the CPU rasterizer
216    /// will see. Pre-sizing keeps later frames allocation-free.
217    pub cpu_max_grid_vsid: u32,
218    /// CPU strip-parallel render thread count (capped to the rayon
219    /// pool). One [`ScratchPool`](roxlap_core::rasterizer::ScratchPool)
220    /// slot per thread.
221    pub cpu_render_threads: usize,
222}
223
224impl Default for RenderOptions {
225    fn default() -> Self {
226        Self {
227            want_gpu: false,
228            gpu: GpuRendererSettings::default(),
229            clear_sky: 0x0099_b3d9,
230            // 32 chunks × CHUNK_SIZE_XY — the scene-demo's widest
231            // combined ground grid.
232            cpu_max_grid_vsid: 32 * roxlap_scene::CHUNK_SIZE_XY,
233            cpu_render_threads: 4,
234        }
235    }
236}
237
238/// Renderer-internal backend; never exposes wgpu or softbuffer types.
239/// The GPU variant owns the whole wgpu device/queue/pipelines, so
240/// it's boxed to keep the enum small.
241enum BackendImpl {
242    // Both variants boxed so the enum stays small regardless of which
243    // backend's state is larger (clippy::large_enum_variant).
244    Cpu(Box<CpuBackend>),
245    Gpu(Box<GpuBackend>),
246}
247
248/// Unified renderer over the CPU and GPU paths. See the crate docs.
249pub struct SceneRenderer {
250    inner: BackendImpl,
251}
252
253impl SceneRenderer {
254    /// Build a renderer for `window` — any [`raw-window-handle`]
255    /// provider (winit, SDL, GLFW, …) in an `Arc`. `size` is the
256    /// window's initial physical framebuffer size in pixels; thereafter
257    /// the host reports changes via [`Self::resize`]. Passing the size
258    /// explicitly keeps the facade decoupled from any one windowing
259    /// library's size API.
260    ///
261    /// Selects the GPU backend when `opts.want_gpu` and WGPU
262    /// initialises; otherwise the CPU backend. **Never fails** — a
263    /// missing/incompatible GPU silently yields the CPU path (the
264    /// message is logged to stderr).
265    ///
266    /// [`raw-window-handle`]: raw_window_handle
267    #[cfg(not(target_arch = "wasm32"))]
268    #[must_use]
269    pub fn new<W>(window: Arc<W>, size: (u32, u32), opts: &RenderOptions) -> Self
270    where
271        W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
272    {
273        if opts.want_gpu {
274            match GpuBackend::new(window.clone(), size, opts) {
275                Ok(g) => {
276                    return Self {
277                        inner: BackendImpl::Gpu(Box::new(g)),
278                    };
279                }
280                Err(e) => {
281                    eprintln!(
282                        "roxlap-render: GPU init failed ({e}); falling back to the CPU renderer",
283                    );
284                }
285            }
286        }
287        Self {
288            inner: BackendImpl::Cpu(Box::new(CpuBackend::new(window, size, opts))),
289        }
290    }
291
292    /// wasm/WebGPU build-time entry: build a renderer over an HTML
293    /// `canvas`. `size` is the canvas's initial framebuffer size in
294    /// pixels; the host reports later changes via [`Self::resize`].
295    ///
296    /// Async because the browser drives wgpu's adapter/device requests
297    /// through its event loop — `await` it inside a
298    /// `wasm_bindgen_futures::spawn_local` task. Selects the GPU
299    /// (WebGPU) backend when `opts.want_gpu` and WebGPU is available;
300    /// otherwise (no WebGPU, or init failed) it falls back to the CPU
301    /// opticast path presented through a WebGL2 blit on the same canvas.
302    /// **Never fails** — the message is logged to the browser console.
303    #[cfg(target_arch = "wasm32")]
304    pub async fn new_from_canvas_async(
305        canvas: web_sys::HtmlCanvasElement,
306        size: (u32, u32),
307        opts: &RenderOptions,
308    ) -> Self {
309        if opts.want_gpu {
310            // `SurfaceTarget::Canvas` moves the canvas into wgpu, so the
311            // GPU attempt gets a clone — the CPU fallback keeps the
312            // original if WebGPU init fails.
313            match GpuBackend::new_async(canvas.clone(), size, opts).await {
314                Ok(g) => {
315                    return Self {
316                        inner: BackendImpl::Gpu(Box::new(g)),
317                    };
318                }
319                Err(e) => {
320                    web_sys::console::warn_1(
321                        &format!("roxlap-render: WebGPU init failed ({e}); using the CPU renderer")
322                            .into(),
323                    );
324                }
325            }
326        }
327        Self {
328            inner: BackendImpl::Cpu(Box::new(CpuBackend::new_from_canvas(canvas, size, opts))),
329        }
330    }
331
332    /// Which backend was selected.
333    #[must_use]
334    pub fn backend(&self) -> Backend {
335        match self.inner {
336            BackendImpl::Cpu(_) => Backend::Cpu,
337            BackendImpl::Gpu(_) => Backend::Gpu,
338        }
339    }
340
341    /// The GPU adapter description when on the GPU backend, else
342    /// `None`.
343    #[must_use]
344    pub fn adapter_info(&self) -> Option<&str> {
345        match &self.inner {
346            BackendImpl::Gpu(g) => Some(g.adapter_info()),
347            BackendImpl::Cpu(_) => None,
348        }
349    }
350
351    /// Upload an equirectangular sky panorama (RGBA8, `w×h`) for the
352    /// GPU marcher's sky sampling. No-op on the CPU backend, which
353    /// samples the [`Sky`] passed in each [`FrameParams`] instead.
354    pub fn set_sky_panorama(&mut self, rgba: &[u8], w: u32, h: u32) {
355        if let BackendImpl::Gpu(g) = &mut self.inner {
356            g.set_sky_panorama(rgba, w, h);
357        }
358    }
359
360    /// Follow a window resize. CPU resizes its framebuffer lazily, so
361    /// this only matters to the GPU swapchain — but it's safe to call
362    /// for both.
363    pub fn resize(&mut self, width: u32, height: u32) {
364        match &mut self.inner {
365            BackendImpl::Cpu(c) => c.resize(width, height),
366            BackendImpl::Gpu(g) => g.resize(width, height),
367        }
368    }
369
370    /// Composite `scene` from `camera` with `frame` params into the
371    /// backend's frame buffer — **without presenting**. The CPU backend
372    /// fills sky + runs the opticast compositor into an owned buffer;
373    /// the GPU backend uploads/refreshes the scene, runs the compute
374    /// marcher + sprite pass, and acquires (but does not present) the
375    /// swapchain frame.
376    ///
377    /// Finish the frame with exactly one of [`present`](Self::present)
378    /// (no overlay) or [`paint_egui`](Self::paint_egui) (UI overlay).
379    /// Calling `render` again without finishing drops the pending frame.
380    pub fn render(&mut self, scene: &mut Scene, camera: &Camera, frame: &FrameParams) {
381        match &mut self.inner {
382            BackendImpl::Cpu(c) => c.render(scene, camera, frame),
383            BackendImpl::Gpu(g) => g.render(scene, camera, frame),
384        }
385    }
386
387    /// Draw world-space [`Line3`] segments over the frame
388    /// [`render`](Self::render) composited, using that frame's camera +
389    /// projection + depth buffer. Call **after** [`render`](Self::render)
390    /// and **before** [`present`](Self::present) /
391    /// [`paint_egui`](Self::paint_egui) — the lines land in the
392    /// framebuffer, so a subsequent `paint_egui` still draws its panels
393    /// on top.
394    ///
395    /// `camera` must be the one the last frame rendered with (the
396    /// projection is taken from that frame). Depth-tested segments
397    /// (`Line3::depth_test`) are occluded by nearer rendered geometry;
398    /// always-on-top segments ignore depth. See [`Line3`] for colour /
399    /// width / blend semantics.
400    pub fn draw_lines(&mut self, camera: &Camera, lines: &[Line3]) {
401        match &mut self.inner {
402            BackendImpl::Cpu(c) => c.draw_lines(camera, lines),
403            BackendImpl::Gpu(g) => g.draw_lines(camera, lines),
404        }
405    }
406
407    /// Present the frame [`render`](Self::render) composited, with no UI
408    /// overlay. Pairs with `render`; use [`paint_egui`](Self::paint_egui)
409    /// instead to overlay an egui UI before presenting.
410    pub fn present(&mut self) {
411        match &mut self.inner {
412            BackendImpl::Cpu(c) => c.present(),
413            BackendImpl::Gpu(g) => g.present(),
414        }
415    }
416
417    /// Overlay an egui UI on the frame [`render`](Self::render)
418    /// composited, then present it (`hud` feature). The host runs egui
419    /// itself (e.g. `egui` + `egui-winit`) and passes the tessellated
420    /// `jobs` ([`egui::Context::tessellate`]) and the per-frame
421    /// `textures` delta from [`egui::FullOutput`]; `pixels_per_point` is
422    /// the UI scale (`ctx.pixels_per_point()`).
423    ///
424    /// The GPU backend paints via `egui-wgpu`; the CPU backend
425    /// software-rasterises the tessellation into its framebuffer. Use
426    /// this **instead of** [`present`](Self::present) — both finish the
427    /// frame.
428    #[cfg(feature = "hud")]
429    pub fn paint_egui(
430        &mut self,
431        jobs: &[egui::ClippedPrimitive],
432        textures: &egui::TexturesDelta,
433        pixels_per_point: f32,
434    ) {
435        match &mut self.inner {
436            BackendImpl::Cpu(c) => c.paint_egui(jobs, textures, pixels_per_point),
437            BackendImpl::Gpu(g) => g.paint_egui(jobs, textures, pixels_per_point),
438        }
439    }
440
441    /// Register sprite models + instances. The CPU backend builds a
442    /// per-instance draw list; the GPU backend builds an instanced
443    /// model registry. Call once at setup (or again to replace).
444    pub fn set_sprites(&mut self, set: &SpriteSet) -> Vec<SpriteModelId> {
445        match &mut self.inner {
446            BackendImpl::Cpu(c) => c.set_sprites(set),
447            BackendImpl::Gpu(g) => g.set_sprites(set),
448        }
449        // Handles are positional by construction (model index = chain id
450        // on both backends), so the facade hands them out directly —
451        // callers keep the handle instead of re-deriving the index.
452        (0..set.models.len()).map(SpriteModelId).collect()
453    }
454
455    /// Re-register one sprite model's geometry after you've edited its
456    /// content (a carve or recolour of its `kv6`). `model` is the
457    /// [`SpriteModelId`] handed back by [`set_sprites`](Self::set_sprites);
458    /// `kv6` is the model's **new** geometry — the caller owns the source
459    /// of truth (e.g. a dense carve grid the surface-only `kv6` can't
460    /// represent) and supplies the refreshed mesh here.
461    ///
462    /// This is a **backend-agnostic content refresh**, not a GPU upload:
463    /// the renderer brings its stored model up to date however its active
464    /// backend needs to. The instance set is left untouched (an edit never
465    /// moves or adds an instance), so on the GPU backend only that one
466    /// model's voxel data is re-uploaded — through a slack-backed
467    /// suballocator, one model's bytes rather than the whole registry —
468    /// while the CPU backend swaps the cached `kv6` into each instance of
469    /// the model. Use [`set_sprites`](Self::set_sprites) to add/remove
470    /// models or change the instance set.
471    pub fn refresh_sprite_model(&mut self, model: SpriteModelId, kv6: &Kv6) {
472        match &mut self.inner {
473            BackendImpl::Cpu(c) => c.update_sprite_model(model.0, kv6),
474            BackendImpl::Gpu(g) => g.update_sprite_model(model.0, kv6),
475        }
476    }
477
478    /// Register animated KFA sprites (one or more bone hierarchies).
479    /// The GPU backend uploads each limb's kv6 as an instanced model
480    /// **once** (appended to the sprite registry) and seeds the limb
481    /// instances at their current pose; the CPU backend caches the
482    /// posed limbs for drawing. Call once at setup, after
483    /// [`set_sprites`](Self::set_sprites), then drive motion per frame
484    /// with [`update_kfa_poses`](Self::update_kfa_poses).
485    ///
486    /// Limbs are posed from the sprites' current
487    /// [`kfaval`](roxlap_formats::kfa::KfaSprite::kfaval) (advance
488    /// [`animsprite`](roxlap_formats::kfa::KfaSprite::animsprite) first
489    /// if using a baked curve), so `kfas` is taken `&mut`.
490    pub fn set_kfa_sprites(&mut self, kfas: &mut [KfaSprite]) {
491        match &mut self.inner {
492            BackendImpl::Cpu(c) => c.set_kfa_sprites(kfas),
493            BackendImpl::Gpu(g) => g.set_kfa_sprites(kfas),
494        }
495    }
496
497    /// Re-pose the registered KFA sprites from their current
498    /// `kfaval[]`. Call each frame after advancing the animation
499    /// (`kfa.animsprite(dt_ms)` or poking `kfaval[]`). The GPU backend
500    /// takes the cheap transform-only update (no model-volume
501    /// re-upload); the CPU backend re-solves limb transforms for the
502    /// next [`render`](Self::render). Must follow a
503    /// [`set_kfa_sprites`](Self::set_kfa_sprites) with the same sprites.
504    pub fn update_kfa_poses(&mut self, kfas: &mut [KfaSprite]) {
505        match &mut self.inner {
506            BackendImpl::Cpu(c) => c.update_kfa_poses(kfas),
507            BackendImpl::Gpu(g) => g.update_kfa_poses(kfas),
508        }
509    }
510
511    /// Carve the next z-layer off the [`SpriteSet::carve_model`] and
512    /// re-upload (the demo's `G` hotkey + GPU.12 copy-on-modify). GPU
513    /// only; a no-op on the CPU backend. Returns the voxels removed.
514    pub fn carve_active_sprite(&mut self) -> u32 {
515        match &mut self.inner {
516            BackendImpl::Cpu(_) => 0,
517            BackendImpl::Gpu(g) => g.carve_active_sprite(),
518        }
519    }
520
521    /// Request that the next [`render`](Self::render) capture its
522    /// framebuffer for [`take_capture`](Self::take_capture). CPU only
523    /// (the GPU swapchain isn't read back) — a no-op on GPU.
524    pub fn request_capture(&mut self) {
525        if let BackendImpl::Cpu(c) = &mut self.inner {
526            c.request_capture();
527        }
528    }
529
530    /// Take the most recently captured frame as packed `0x00RRGGBB`
531    /// pixels + dimensions, or `None` if no capture is ready / GPU.
532    pub fn take_capture(&mut self) -> Option<(Vec<u32>, u32, u32)> {
533        match &mut self.inner {
534            BackendImpl::Cpu(c) => c.take_capture(),
535            BackendImpl::Gpu(_) => None,
536        }
537    }
538
539    /// Screen→world picking input: the world-space hit distance `t` at
540    /// window pixel `(x, y)` from the **last rendered frame**, or `None`
541    /// for out-of-bounds pixels and sky / no-hit. The host reconstructs
542    /// the world hit point as `cam.pos + t * normalize(ray_dir)`, where
543    /// `ray_dir` is the same per-pixel ray the frame was rendered with
544    /// (see the backend's projection).
545    ///
546    /// `t` is the distance to the nearest **scene-grid** surface
547    /// (terrain + grids); sprites do not occlude it (the sprite pass
548    /// reads depth read-only), so a cursor sprite under the pointer is
549    /// transparent to the pick.
550    ///
551    /// Cost: the CPU backend reads its in-memory z-buffer (free); the
552    /// GPU backend stages the depth buffer and blocks on a device poll
553    /// (cheap at click time — do not call every frame). The GPU path
554    /// only has depth when the last frame drew sprites (`write_depth`).
555    #[must_use]
556    pub fn pick_depth(&self, x: u32, y: u32) -> Option<f32> {
557        match &self.inner {
558            BackendImpl::Cpu(c) => c.pick_depth(x, y),
559            BackendImpl::Gpu(g) => g.pick_depth(x, y),
560        }
561    }
562
563    /// World-space view-ray direction (un-normalised) for window pixel
564    /// `(x, y)`, under the projection the **last frame** rendered with.
565    /// The backends differ (CPU `setcamera` vs GPU vertical-FOV
566    /// pinhole), so this hides which one is active. `None` before the
567    /// first frame. Intersect it with a plane for tile picking, or feed
568    /// it to [`Self::pick`] for a voxel.
569    #[must_use]
570    pub fn pixel_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<[f64; 3]> {
571        match &self.inner {
572            BackendImpl::Cpu(c) => c.pixel_ray(camera, x, y),
573            BackendImpl::Gpu(g) => g.pixel_ray(camera, x, y),
574        }
575    }
576
577    /// Canonical screen→world unproject: the full view [`Ray`]
578    /// (`camera.pos` origin + unit direction) for window pixel
579    /// `(x, y)`, under whichever projection the last frame used. The
580    /// one entry point both backends honour — hosts never reconstruct
581    /// the projection. `None` before the first frame or for a
582    /// degenerate ray.
583    ///
584    /// Compose with [`roxlap_scene::Scene::raycast`] for depth-free
585    /// picking that's identical on CPU and GPU:
586    /// `renderer.view_ray(cam, x, y).and_then(|r| scene.raycast(r.origin, r.dir, max))`.
587    #[must_use]
588    pub fn view_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<Ray> {
589        let d = self.pixel_ray(camera, x, y)?;
590        let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
591        if len < 1e-12 {
592            return None;
593        }
594        Some(Ray {
595            origin: glam::DVec3::from_array([camera.pos[0], camera.pos[1], camera.pos[2]]),
596            dir: glam::DVec3::new(d[0] / len, d[1] / len, d[2] / len),
597        })
598    }
599
600    /// One-call screen→world voxel pick: unproject pixel `(x, y)` with
601    /// the active backend's projection, read the last frame's depth
602    /// there, reconstruct the world hit, and resolve it to the owning
603    /// grid + grid-local voxel via [`Scene::resolve_voxel`]. `None` on
604    /// sky / no-hit, or when no grid claims the surface.
605    ///
606    /// `scene` and `camera` must be the ones the last frame rendered;
607    /// the projection (size + FOV / `hx,hy,hz`) is taken from that
608    /// frame. Cheap on CPU (in-memory z-buffer); on GPU it stages the
609    /// depth buffer (a click-time device poll — not per frame).
610    #[must_use]
611    pub fn pick(&self, scene: &Scene, camera: &Camera, x: u32, y: u32) -> Option<PickHit> {
612        let dir = self.pixel_ray(camera, f64::from(x), f64::from(y))?;
613        let t = f64::from(self.pick_depth(x, y)?);
614        let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
615        if len < 1e-9 {
616            return None;
617        }
618        let s = t / len; // world = cam.pos + t · (dir / |dir|)
619        let world = glam::DVec3::new(
620            camera.pos[0] + dir[0] * s,
621            camera.pos[1] + dir[1] * s,
622            camera.pos[2] + dir[2] * s,
623        );
624        let (grid, voxel) = scene.resolve_voxel(world, glam::DVec3::from_array(dir))?;
625        #[allow(clippy::cast_possible_truncation)]
626        let world_f32 = [world.x as f32, world.y as f32, world.z as f32];
627        Some(PickHit {
628            world: world_f32,
629            grid,
630            voxel,
631        })
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    #[test]
640    fn options_default_is_cpu_intent() {
641        let o = RenderOptions::default();
642        assert!(!o.want_gpu);
643        assert_eq!(o.clear_sky & 0xFF00_0000, 0, "clear_sky is 0x00RRGGBB");
644    }
645}