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//! This is the RF.0 skeleton: backend selection + fallback + a
17//! clear-to-sky frame. RF.1/RF.2 fill in the real CPU/GPU scene
18//! render; RF.3 adds sprites; RF.4 adds framebuffer capture.
19
20#![forbid(unsafe_code)]
21
22mod cpu;
23mod gpu;
24
25use std::sync::Arc;
26
27use winit::window::Window;
28
29use roxlap_core::opticast::OpticastSettings;
30use roxlap_core::sky::Sky;
31use roxlap_core::sprite::SpriteLighting;
32use roxlap_core::Camera;
33use roxlap_scene::Scene;
34
35pub use roxlap_formats::sprite::Sprite;
36pub use roxlap_gpu::{GpuInitError, GpuRendererSettings};
37
38use crate::cpu::CpuBackend;
39use crate::gpu::GpuBackend;
40
41/// One placed sprite instance: which [`SpriteSet::models`] entry and
42/// where in the world.
43pub struct SpriteInstanceDesc {
44 pub model: usize,
45 pub pos: [f32; 3],
46}
47
48/// Backend-agnostic sprite description. The facade builds the CPU
49/// per-instance draw list and the GPU instanced registry from the
50/// same data, so both backends show identical sprites. The host owns
51/// content (which models, where, recolouring) — building a recoloured
52/// variant is just a second [`Sprite`] model with edited `kv6.voxels`.
53pub struct SpriteSet {
54 /// Distinct voxel models (KV6 + base orientation). Instances index
55 /// into this; their position overrides the model's.
56 pub models: Vec<Sprite>,
57 pub instances: Vec<SpriteInstanceDesc>,
58 /// Model the [`SceneRenderer::carve_active_sprite`] hotkey edits
59 /// (GPU only, mirroring the demo's `G`-carve). `None` disables it.
60 pub carve_model: Option<usize>,
61}
62
63/// Per-frame inputs both backends consume. The host builds the
64/// [`OpticastSettings`] (it owns scan distance etc.); the facade does
65/// everything else (pool config, sky fill, render, present).
66pub struct FrameParams<'a> {
67 /// CPU opticast settings (scan distance, mip ladder, framebuffer
68 /// geometry). Ignored by the GPU backend.
69 pub settings: &'a OpticastSettings,
70 /// Packed engine sky colour: the CPU sky-miss fill + skycast, and
71 /// the clear colour if no scene renders.
72 pub sky_color: u32,
73 /// Optional sky panorama for the CPU rasterizer's sky sampling.
74 pub sky: Option<&'a Sky>,
75 /// CPU fog: packed colour + max scan distance (voxels). `0` scan
76 /// distance disables CPU fog.
77 pub fog_color: u32,
78 pub fog_max_scan_dist: i32,
79 /// CPU: treat z=255 as air (avoids the S1.X bedrock path for
80 /// out-of-bounds cameras).
81 pub treat_z_max_as_air: bool,
82 /// GPU scene-grid LOD scan distance (world units); see GPU.11.1.
83 /// Ignored by the CPU backend.
84 pub gpu_mip_scan_dist: f32,
85 /// GPU outer-DDA step budget (chunks). Ignored by the CPU backend.
86 pub gpu_max_outer_steps: u32,
87 /// GPU vertical field of view (radians). Ignored by the CPU
88 /// backend (it derives projection from [`OpticastSettings`]).
89 pub gpu_fov_y_rad: f32,
90 /// CPU sprite shading (built by the host from its engine). Required
91 /// for the CPU backend to draw sprites; ignored by the GPU backend
92 /// (its sprite pass shades from the uploaded model colours). `None`
93 /// skips CPU sprite drawing.
94 pub sprite_lighting: Option<&'a SpriteLighting<'a>>,
95}
96
97/// Which renderer a [`SceneRenderer`] resolved to at construction.
98#[derive(Clone, Copy, PartialEq, Eq, Debug)]
99pub enum Backend {
100 /// `roxlap-core` opticast, presented via `softbuffer`.
101 Cpu,
102 /// `roxlap-gpu` compute marcher, presented via wgpu.
103 Gpu,
104}
105
106/// Construction-time options for [`SceneRenderer::new`].
107pub struct RenderOptions {
108 /// Try the GPU backend first. When `false`, or when GPU init
109 /// fails, the renderer uses the CPU backend.
110 pub want_gpu: bool,
111 /// Settings forwarded to [`roxlap_gpu::GpuRenderer`] when the GPU
112 /// backend is selected.
113 pub gpu: GpuRendererSettings,
114 /// Packed `0x00RRGGBB` (alpha ignored) the empty/clear frame fills
115 /// with until a scene render lands. Also the CPU sky-miss colour
116 /// default if a frame supplies none.
117 pub clear_sky: u32,
118 /// CPU [`ScratchPool`](roxlap_core::rasterizer::ScratchPool) `lastx`
119 /// sizing — the largest combined grid `vsid` the CPU rasterizer
120 /// will see. Pre-sizing keeps later frames allocation-free.
121 pub cpu_max_grid_vsid: u32,
122 /// CPU strip-parallel render thread count (capped to the rayon
123 /// pool). One [`ScratchPool`](roxlap_core::rasterizer::ScratchPool)
124 /// slot per thread.
125 pub cpu_render_threads: usize,
126}
127
128impl Default for RenderOptions {
129 fn default() -> Self {
130 Self {
131 want_gpu: false,
132 gpu: GpuRendererSettings::default(),
133 clear_sky: 0x0099_b3d9,
134 // 32 chunks × CHUNK_SIZE_XY — the scene-demo's widest
135 // combined ground grid.
136 cpu_max_grid_vsid: 32 * roxlap_scene::CHUNK_SIZE_XY,
137 cpu_render_threads: 4,
138 }
139 }
140}
141
142/// Renderer-internal backend; never exposes wgpu or softbuffer types.
143/// The GPU variant owns the whole wgpu device/queue/pipelines, so
144/// it's boxed to keep the enum small.
145enum BackendImpl {
146 Cpu(CpuBackend),
147 Gpu(Box<GpuBackend>),
148}
149
150/// Unified renderer over the CPU and GPU paths. See the crate docs.
151pub struct SceneRenderer {
152 inner: BackendImpl,
153}
154
155impl SceneRenderer {
156 /// Build a renderer for `window`. Selects the GPU backend when
157 /// `opts.want_gpu` and WGPU initialises; otherwise the CPU
158 /// backend. **Never fails** — a missing/incompatible GPU silently
159 /// yields the CPU path (the message is logged to stderr).
160 #[must_use]
161 pub fn new(window: Arc<Window>, opts: &RenderOptions) -> Self {
162 if opts.want_gpu {
163 match GpuBackend::new(window.clone(), opts) {
164 Ok(g) => {
165 return Self {
166 inner: BackendImpl::Gpu(Box::new(g)),
167 };
168 }
169 Err(e) => {
170 eprintln!(
171 "roxlap-render: GPU init failed ({e}); falling back to the CPU renderer",
172 );
173 }
174 }
175 }
176 Self {
177 inner: BackendImpl::Cpu(CpuBackend::new(window, opts)),
178 }
179 }
180
181 /// Which backend was selected.
182 #[must_use]
183 pub fn backend(&self) -> Backend {
184 match self.inner {
185 BackendImpl::Cpu(_) => Backend::Cpu,
186 BackendImpl::Gpu(_) => Backend::Gpu,
187 }
188 }
189
190 /// The GPU adapter description when on the GPU backend, else
191 /// `None`.
192 #[must_use]
193 pub fn adapter_info(&self) -> Option<&str> {
194 match &self.inner {
195 BackendImpl::Gpu(g) => Some(g.adapter_info()),
196 BackendImpl::Cpu(_) => None,
197 }
198 }
199
200 /// Upload an equirectangular sky panorama (RGBA8, `w×h`) for the
201 /// GPU marcher's sky sampling. No-op on the CPU backend, which
202 /// samples the [`Sky`] passed in each [`FrameParams`] instead.
203 pub fn set_sky_panorama(&mut self, rgba: &[u8], w: u32, h: u32) {
204 if let BackendImpl::Gpu(g) = &mut self.inner {
205 g.set_sky_panorama(rgba, w, h);
206 }
207 }
208
209 /// Follow a window resize. CPU resizes its framebuffer lazily, so
210 /// this only matters to the GPU swapchain — but it's safe to call
211 /// for both.
212 pub fn resize(&mut self, width: u32, height: u32) {
213 match &mut self.inner {
214 BackendImpl::Cpu(c) => c.resize(width, height),
215 BackendImpl::Gpu(g) => g.resize(width, height),
216 }
217 }
218
219 /// Render `scene` from `view` with `frame` params and present to
220 /// the window. The CPU backend fills sky, runs the opticast
221 /// compositor, and presents via softbuffer; the GPU backend
222 /// uploads/refreshes the scene and runs the compute marcher, then
223 /// the sprite pass.
224 pub fn render(&mut self, scene: &mut Scene, camera: &Camera, frame: &FrameParams) {
225 match &mut self.inner {
226 BackendImpl::Cpu(c) => c.render(scene, camera, frame),
227 BackendImpl::Gpu(g) => g.render(scene, camera, frame),
228 }
229 }
230
231 /// Register sprite models + instances. The CPU backend builds a
232 /// per-instance draw list; the GPU backend builds an instanced
233 /// model registry. Call once at setup (or again to replace).
234 pub fn set_sprites(&mut self, set: &SpriteSet) {
235 match &mut self.inner {
236 BackendImpl::Cpu(c) => c.set_sprites(set),
237 BackendImpl::Gpu(g) => g.set_sprites(set),
238 }
239 }
240
241 /// Carve the next z-layer off the [`SpriteSet::carve_model`] and
242 /// re-upload (the demo's `G` hotkey + GPU.12 copy-on-modify). GPU
243 /// only; a no-op on the CPU backend. Returns the voxels removed.
244 pub fn carve_active_sprite(&mut self) -> u32 {
245 match &mut self.inner {
246 BackendImpl::Cpu(_) => 0,
247 BackendImpl::Gpu(g) => g.carve_active_sprite(),
248 }
249 }
250
251 /// Request that the next [`render`](Self::render) capture its
252 /// framebuffer for [`take_capture`](Self::take_capture). CPU only
253 /// (the GPU swapchain isn't read back) — a no-op on GPU.
254 pub fn request_capture(&mut self) {
255 if let BackendImpl::Cpu(c) = &mut self.inner {
256 c.request_capture();
257 }
258 }
259
260 /// Take the most recently captured frame as packed `0x00RRGGBB`
261 /// pixels + dimensions, or `None` if no capture is ready / GPU.
262 pub fn take_capture(&mut self) -> Option<(Vec<u32>, u32, u32)> {
263 match &mut self.inner {
264 BackendImpl::Cpu(c) => c.take_capture(),
265 BackendImpl::Gpu(_) => None,
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn options_default_is_cpu_intent() {
276 let o = RenderOptions::default();
277 assert!(!o.want_gpu);
278 assert_eq!(o.clear_sky & 0xFF00_0000, 0, "clear_sky is 0x00RRGGBB");
279 }
280}