Skip to main content

roxlap_gpu/
lib.rs

1//! WGPU-backed compute-shader renderer scaffold for the roxlap
2//! voxel engine. GPU.1 in `PORTING-GPU.md`.
3//!
4//! GPU.1's job: stand up the device + surface + swapchain on a
5//! winit window, present a clear-to-colour frame each render call,
6//! and give the host a one-call opt-in. No voxel marching yet — the
7//! [`examples/probe.rs`](../examples/probe.rs) standalone holds
8//! the empirical FPS baseline from GPU.0.
9//!
10//! Later sub-substages flesh `GpuRenderer::render` out: GPU.2
11//! uploads voxel data, GPU.3 dispatches the inner-DDA compute
12//! shader, GPU.4 layers in chunk skipping, GPU.5 plugs the renderer
13//! into `roxlap-scene::Scene`, …
14//!
15//! ## Host integration shape (GPU.1)
16//!
17//! ```no_run
18//! use std::sync::Arc;
19//! use roxlap_gpu::{GpuRenderer, GpuRendererSettings};
20//! # use winit::window::Window;
21//! # fn pick(w: Arc<Window>) -> Option<GpuRenderer> {
22//! match GpuRenderer::new_blocking(w, GpuRendererSettings::default()) {
23//!     Ok(r) => Some(r),
24//!     Err(e) => {
25//!         eprintln!("GPU init failed: {e}; falling back to CPU");
26//!         None
27//!     }
28//! }
29//! # }
30//! ```
31
32#![allow(clippy::must_use_candidate, clippy::too_many_lines)]
33
34pub mod camera;
35pub mod decompress;
36pub mod grid;
37pub mod headless;
38pub mod resident;
39pub mod scene;
40pub mod sprite_model;
41
42pub use camera::Camera;
43pub use decompress::{decompress_chunk, ChunkUpload, BEDROCK_RGB, CHUNK_Z};
44pub use grid::{bounding_box_of, GpuGridResident, GridUpload};
45pub use headless::HeadlessGpu;
46pub use resident::GpuChunkResident;
47pub use scene::{
48    GpuSceneResident, GridRuntimeTransform, GridStaticMeta, RefreshOutcome, SceneUpload,
49    MAX_SCENE_GRIDS,
50};
51pub use sprite_model::{
52    build_sprite_model, SpriteInstance, SpriteInstanceTransform, SpriteModel, SpriteModelRegistry,
53    SpriteRegistryResident,
54};
55
56use std::sync::Arc;
57
58use bytemuck::{Pod, Zeroable};
59use winit::window::Window;
60
61/// Caller-controllable knobs for [`GpuRenderer::new`]. Defaults
62/// target "highest-performance GPU, prefer Mailbox/Immediate over
63/// vsync" — i.e. the same configuration the GPU.0 probe used to
64/// measure the FPS ceiling.
65#[derive(Debug, Clone, Copy)]
66pub struct GpuRendererSettings {
67    pub power_preference: PowerPreference,
68    /// Initial clear colour cycled by GPU.1's empty render path.
69    /// The voxel-rendering substages overwrite this entirely.
70    pub clear_colour: [f64; 3],
71    /// Prefer mailbox/immediate when offered; falls back to FIFO if
72    /// the surface only supports it (Wayland under Mesa often does).
73    pub uncapped_present: bool,
74}
75
76#[derive(Debug, Clone, Copy)]
77pub enum PowerPreference {
78    Low,
79    High,
80}
81
82impl Default for GpuRendererSettings {
83    fn default() -> Self {
84        Self {
85            power_preference: PowerPreference::High,
86            clear_colour: [0.06, 0.08, 0.12],
87            uncapped_present: true,
88        }
89    }
90}
91
92/// Errors `GpuRenderer::new` surfaces to the host. The host's
93/// expected flow is "try this, fall back to the CPU path on Err".
94#[derive(Debug)]
95pub enum GpuInitError {
96    CreateSurface(wgpu::CreateSurfaceError),
97    NoAdapter,
98    RequestDevice(wgpu::RequestDeviceError),
99}
100
101impl std::fmt::Display for GpuInitError {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            Self::CreateSurface(e) => write!(f, "create_surface failed: {e}"),
105            Self::NoAdapter => write!(
106                f,
107                "no compatible adapter — does this system have a Vulkan/Metal/DX12 driver?"
108            ),
109            Self::RequestDevice(e) => write!(f, "request_device failed: {e}"),
110        }
111    }
112}
113
114impl std::error::Error for GpuInitError {
115    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116        match self {
117            Self::CreateSurface(e) => Some(e),
118            Self::RequestDevice(e) => Some(e),
119            Self::NoAdapter => None,
120        }
121    }
122}
123
124impl From<wgpu::CreateSurfaceError> for GpuInitError {
125    fn from(value: wgpu::CreateSurfaceError) -> Self {
126        Self::CreateSurface(value)
127    }
128}
129
130impl From<wgpu::RequestDeviceError> for GpuInitError {
131    fn from(value: wgpu::RequestDeviceError) -> Self {
132        Self::RequestDevice(value)
133    }
134}
135
136/// WGPU-backed renderer. Owns the device, queue, and surface
137/// bound to the host's winit window. [`Self::render`] is the GPU.1
138/// clear-to-colour path; [`Self::render_chunk`] is GPU.3's
139/// single-chunk DDA marcher.
140pub struct GpuRenderer {
141    window: Arc<Window>,
142    surface: wgpu::Surface<'static>,
143    surface_config: wgpu::SurfaceConfiguration,
144    device: wgpu::Device,
145    queue: wgpu::Queue,
146    adapter_info: String,
147    clear_colour: [f64; 3],
148    frame_count: u32,
149    /// Lazy-built on first [`Self::render_chunk`] call; rebuilt when
150    /// the swapchain resizes (storage texture must match).
151    chunk_dda: Option<ChunkDdaResources>,
152    /// Lazy-built on first [`Self::render_grid`] call; same resize
153    /// trigger as `chunk_dda`. The two paths share the same blit
154    /// pipeline structure but bind different storage layouts.
155    grid_dda: Option<GridDdaResources>,
156    /// Lazy-built on first [`Self::render_scene`] call. Holds the
157    /// multi-grid pipeline + per-grid camera uniforms.
158    scene_dda: Option<SceneDdaResources>,
159    /// GPU.8 — panoramic sky texture + sampler. Created at
160    /// `new` as a 1×1 mid-grey default; [`Self::set_sky_panorama`]
161    /// replaces it. The scene-DDA bind group references this each
162    /// frame.
163    sky_texture: wgpu::Texture,
164    sky_view: wgpu::TextureView,
165    sky_sampler: wgpu::Sampler,
166    /// GPU.8 fog state. `color` is BGRA-style premultiplied (each
167    /// channel in [0, 1]); `near` is the world-t distance at which
168    /// fog starts kicking in; `far` is the distance at which it's
169    /// fully opaque. The shader does
170    /// `mix(hit, fog, smoothstep(near, far, t))`.
171    fog_color: [f32; 3],
172    fog_near: f32,
173    fog_far: f32,
174    /// GPU.10 — sprites rendered as DDA-marched voxel models (the
175    /// precise path; the GPU.9 compute splatter it replaced was
176    /// retired in 10.5). Holds the concatenated model registry + the
177    /// per-frame instance array; set via [`Self::set_sprite_instances`].
178    sprite_registry: Option<sprite_model::SpriteRegistryResident>,
179    /// Lazy-built pipeline + uniform for the model-DDA pass.
180    sprite_model_dda: Option<SpriteModelDdaResources>,
181    /// GPU.10.4 — LOD aggressiveness: step a sprite to the next mip
182    /// once a mip-0 voxel projects below this many screen pixels.
183    /// Defaults to 4.0 (the empirical sweet spot); the host can tune
184    /// via [`Self::set_sprite_lod_px`].
185    sprite_lod_px: f32,
186    /// GPU.11.1 — scene-grid LOD scan distance (world units). A chunk
187    /// entered at world-t `t` is marched at the mip level
188    /// `floor(log2(max(t, msd) / msd))`, clamped to the grid's mip
189    /// ladder. `0` disables LOD (always mip-0). Tunable via
190    /// [`Self::set_scene_mip_scan_dist`] — the axis-aligned-mip-beams
191    /// mitigation (GPU.11.2) pushes it outward if banding appears.
192    scene_mip_scan_dist: f32,
193}
194
195/// Per-renderer chunk-DDA pipeline state. The compute shader writes
196/// into the storage texture; a fullscreen-triangle render pass
197/// nearest-neighbour blits it to the swapchain.
198struct ChunkDdaResources {
199    storage_size: (u32, u32),
200    storage_view: wgpu::TextureView,
201    uniform_buf: wgpu::Buffer,
202    bgl_dda: wgpu::BindGroupLayout,
203    pipeline_dda: wgpu::ComputePipeline,
204    blit_bg: wgpu::BindGroup,
205    pipeline_blit: wgpu::RenderPipeline,
206    // wgpu BindGroups internally Arc their resources, but we keep
207    // the handle so the sampler shows up in profiler dumps.
208    _sampler: wgpu::Sampler,
209}
210
211struct GridDdaResources {
212    storage_size: (u32, u32),
213    storage_view: wgpu::TextureView,
214    uniform_buf: wgpu::Buffer,
215    bgl_dda: wgpu::BindGroupLayout,
216    pipeline_dda: wgpu::ComputePipeline,
217    blit_bg: wgpu::BindGroup,
218    pipeline_blit: wgpu::RenderPipeline,
219    _sampler: wgpu::Sampler,
220}
221
222struct SceneDdaResources {
223    storage_size: (u32, u32),
224    storage_view: wgpu::TextureView,
225    uniform_buf: wgpu::Buffer,
226    bgl_dda: wgpu::BindGroupLayout,
227    pipeline_dda: wgpu::ComputePipeline,
228    blit_bg: wgpu::BindGroup,
229    pipeline_blit: wgpu::RenderPipeline,
230    _sampler: wgpu::Sampler,
231    /// GPU.9 — per-pixel world-t depth (f32 bits as u32), sized
232    /// `width * height * 4`. The scene pass writes it when sprites
233    /// are present; the sprite model-DDA pass reads + composites
234    /// against it.
235    depth_buffer: wgpu::Buffer,
236}
237
238/// GPU.10.0 — single-sprite model-DDA pipeline: one thread per pixel
239/// marches the model voxel volume and composites against the scene
240/// depth buffer.
241struct SpriteModelDdaResources {
242    bgl: wgpu::BindGroupLayout,
243    pipeline: wgpu::ComputePipeline,
244    uniform_buf: wgpu::Buffer,
245}
246
247/// Per-frame uniform for the model-DDA pass. Mirrors `Uniform` in
248/// `sprite_model_dda.wgsl` (std140). Per-model + per-instance data
249/// now live in storage buffers; this holds only the camera, fog, and
250/// instance count.
251#[repr(C)]
252#[derive(Clone, Copy, Pod, Zeroable)]
253struct SpriteModelUniform {
254    cam_pos: [f32; 3],
255    _p0: f32,
256    cam_right: [f32; 3],
257    _p1: f32,
258    cam_down: [f32; 3],
259    _p2: f32,
260    cam_forward: [f32; 3],
261    _p3: f32,
262    fog_color: [f32; 4],
263    screen_size: [u32; 2],
264    instance_count: u32,
265    fog_far: f32,
266    fov_y_rad: f32,
267    tiles_x: u32,
268    tile_size: u32,
269    _p6: f32,
270}
271
272const SCENE_MAX_GRIDS: usize = MAX_SCENE_GRIDS as usize;
273
274/// GPU.10.3 — sprite screen-tile edge in pixels for instance binning.
275const SPRITE_TILE_SIZE: u32 = 16;
276
277// The scene_dda bind group + layout wire occupancy pages 1..=3 at
278// bindings 12..=14 explicitly; keep that in lockstep with the page
279// count. Bump the bindings (here, in the WGSL, and in the bind
280// group) if MAX_OCC_PAGES changes.
281const _: () = assert!(scene::MAX_OCC_PAGES == 4);
282
283#[repr(C)]
284#[derive(Clone, Copy, Pod, Zeroable)]
285struct SceneDdaPerGridCamera {
286    pos: [f32; 3],
287    _pad0: f32,
288    right: [f32; 3],
289    _pad1: f32,
290    down: [f32; 3],
291    _pad2: f32,
292    forward: [f32; 3],
293    _pad3: f32,
294}
295
296#[repr(C)]
297#[derive(Clone, Copy, Pod, Zeroable)]
298struct SceneDdaUniform {
299    fov_y_rad: f32,
300    grid_count: u32,
301    max_outer_steps: u32,
302    _pad0: u32,
303    screen_size: [u32; 2],
304    _pad1: [u32; 2],
305    cameras: [SceneDdaPerGridCamera; SCENE_MAX_GRIDS],
306    /// GPU.8 — `[r, g, b, fog_near]`. The `near` distance is packed
307    /// into the colour's alpha channel to keep std140 alignment
308    /// tidy (a bare `f32` after the `vec4` would force extra pads).
309    fog_color: [f32; 4],
310    fog_far: f32,
311    /// GPU.9 — `1` when the sprite pass is active (scene pass then
312    /// records `best_t` into the depth buffer), `0` otherwise.
313    write_depth: u32,
314    /// Occupancy paging: words per storage page (see
315    /// `scene::split_occupancy_pages`). Only consulted by the shader
316    /// when `occ_num_pages > 1`.
317    occ_page_words: u32,
318    /// Number of real occupancy pages (1 on multi-GiB GPUs → the
319    /// shader takes a branch-free single-page read).
320    occ_num_pages: u32,
321    /// GPU.11.1 — scene-grid LOD scan distance (world units). A chunk
322    /// entered at world-t `t` marches at mip
323    /// `floor(log2(max(t, msd) / msd))`, clamped to the grid's mip
324    /// count. `0` disables LOD (always mip-0).
325    mip_scan_dist: f32,
326    _pad2: u32,
327    _pad3: u32,
328    _pad4: u32,
329}
330
331#[repr(C)]
332#[derive(Clone, Copy, Pod, Zeroable)]
333struct GridDdaUniform {
334    camera_pos: [f32; 3],
335    _pad0: f32,
336    camera_right: [f32; 3],
337    _pad1: f32,
338    camera_down: [f32; 3],
339    _pad2: f32,
340    camera_forward: [f32; 3],
341    fov_y_rad: f32,
342    screen_size: [u32; 2],
343    vsid: u32,
344    max_outer_steps: u32,
345    chunks_dims: [u32; 3],
346    _pad3: u32,
347    origin_chunk: [i32; 3],
348    _pad4: u32,
349}
350
351#[repr(C)]
352#[derive(Clone, Copy, Pod, Zeroable)]
353struct ChunkDdaUniform {
354    camera_pos: [f32; 3],
355    _pad0: f32,
356    camera_right: [f32; 3],
357    _pad1: f32,
358    camera_down: [f32; 3],
359    _pad2: f32,
360    camera_forward: [f32; 3],
361    fov_y_rad: f32,
362    screen_size: [u32; 2],
363    vsid: u32,
364    max_scan_dist: u32,
365}
366
367impl GpuRenderer {
368    /// Stand up the device + surface + swapchain on `window`. Async
369    /// because `wgpu::Adapter`/`Device` requests are.
370    ///
371    /// # Errors
372    /// Returns [`GpuInitError`] if surface creation, adapter
373    /// selection, or device request fails. Hosts treat any error as
374    /// "fall back to the CPU path".
375    pub async fn new(
376        window: Arc<Window>,
377        settings: GpuRendererSettings,
378    ) -> Result<Self, GpuInitError> {
379        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
380        let surface = instance.create_surface(window.clone())?;
381        let power_preference = match settings.power_preference {
382            PowerPreference::Low => wgpu::PowerPreference::LowPower,
383            PowerPreference::High => wgpu::PowerPreference::HighPerformance,
384        };
385        let adapter = instance
386            .request_adapter(&wgpu::RequestAdapterOptions {
387                power_preference,
388                compatible_surface: Some(&surface),
389                force_fallback_adapter: false,
390            })
391            .await
392            .ok_or(GpuInitError::NoAdapter)?;
393
394        let info = adapter.get_info();
395        let adapter_info = format!(
396            "{name} ({backend:?}, {device_type:?})",
397            name = info.name,
398            backend = info.backend,
399            device_type = info.device_type,
400        );
401
402        let (device, queue) = adapter
403            .request_device(
404                &wgpu::DeviceDescriptor {
405                    label: Some("roxlap-gpu device"),
406                    required_features: wgpu::Features::empty(),
407                    required_limits: pick_required_limits(&adapter.limits()),
408                    memory_hints: wgpu::MemoryHints::default(),
409                },
410                None,
411            )
412            .await?;
413
414        let caps = surface.get_capabilities(&adapter);
415        // Pick a NON-sRGB swapchain format. Voxlap colours are
416        // already sRGB-encoded (the slab bytes are display-ready,
417        // matching what the CPU softbuffer path writes straight to
418        // the framebuffer with no conversion). An sRGB swapchain
419        // would re-apply the gamma curve on top, producing a
420        // washed-out / pastel look that diverges from the CPU
421        // renderer. Falls back to `caps.formats[0]` only if every
422        // offered format is sRGB.
423        let surface_format = caps
424            .formats
425            .iter()
426            .copied()
427            .find(|f| !f.is_srgb())
428            .unwrap_or(caps.formats[0]);
429        let present_mode = if settings.uncapped_present {
430            pick_present_mode(&caps.present_modes)
431        } else {
432            wgpu::PresentMode::Fifo
433        };
434        // GPU.11.2 — surface the present mode: `Fifo` is vsync-capped
435        // (FPS pinned to refresh rate → compute optimisations like the
436        // mip LOD won't show up in the FPS counter). Mailbox/Immediate
437        // are uncapped. Wayland under Mesa frequently offers only Fifo.
438        eprintln!(
439            "roxlap-gpu: present mode = {present_mode:?} (available: {:?})",
440            caps.present_modes,
441        );
442        let physical = window.inner_size();
443        let surface_config = wgpu::SurfaceConfiguration {
444            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
445            format: surface_format,
446            width: physical.width.max(1),
447            height: physical.height.max(1),
448            present_mode,
449            alpha_mode: caps.alpha_modes[0],
450            view_formats: vec![],
451            desired_maximum_frame_latency: 2,
452        };
453        surface.configure(&device, &surface_config);
454
455        // GPU.8 default sky: a 1×1 mid-grey texture. Hosts replace
456        // it via `set_sky_panorama` with a real equirectangular
457        // panorama; the default stops the shader sampling
458        // uninitialised memory before that happens.
459        let default_sky_pixel = [0x80u8, 0x80, 0x80, 0xff];
460        let (sky_texture, sky_view) = create_sky_texture(&device, 1, 1, &default_sky_pixel);
461        queue.write_texture(
462            wgpu::ImageCopyTexture {
463                texture: &sky_texture,
464                mip_level: 0,
465                origin: wgpu::Origin3d::ZERO,
466                aspect: wgpu::TextureAspect::All,
467            },
468            &default_sky_pixel,
469            wgpu::ImageDataLayout {
470                offset: 0,
471                bytes_per_row: Some(4),
472                rows_per_image: Some(1),
473            },
474            wgpu::Extent3d {
475                width: 1,
476                height: 1,
477                depth_or_array_layers: 1,
478            },
479        );
480        let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
481            label: Some("roxlap-gpu sky_sampler"),
482            // Voxlap-convention panorama: u = elevation [0, 1]
483            // (Repeat is a no-op since values don't go outside),
484            // v = azimuth (wraps 360° — Repeat is required).
485            address_mode_u: wgpu::AddressMode::Repeat,
486            address_mode_v: wgpu::AddressMode::Repeat,
487            address_mode_w: wgpu::AddressMode::ClampToEdge,
488            mag_filter: wgpu::FilterMode::Linear,
489            min_filter: wgpu::FilterMode::Linear,
490            mipmap_filter: wgpu::FilterMode::Nearest,
491            ..Default::default()
492        });
493
494        Ok(Self {
495            window,
496            surface,
497            surface_config,
498            device,
499            queue,
500            adapter_info,
501            clear_colour: settings.clear_colour,
502            frame_count: 0,
503            chunk_dda: None,
504            grid_dda: None,
505            scene_dda: None,
506            sky_texture,
507            sky_view,
508            sky_sampler,
509            // Fog disabled by default — voxlap's CPU rasterizer
510            // also runs without fog in the scene-demo, so matching
511            // it means no GPU fog out of the box. Hosts can opt in
512            // via `set_fog` (e.g. for atmospheric far-LOD masking).
513            fog_color: [0.66, 0.74, 0.88],
514            fog_near: 0.0,
515            fog_far: 1.0e30,
516            sprite_registry: None,
517            sprite_model_dda: None,
518            // GPU.10.4 — default LOD threshold: step to a coarser mip
519            // once a voxel projects below 4 px. Empirically the best
520            // quality/cost tradeoff; the host can override.
521            sprite_lod_px: 4.0,
522            // GPU.11.1 — matches the CPU demo's mip_scan_dist=64.
523            scene_mip_scan_dist: 64.0,
524        })
525    }
526
527    /// Synchronous wrapper for hosts that don't have an async
528    /// runtime. Internally `pollster::block_on`s [`Self::new`].
529    ///
530    /// # Errors
531    /// See [`Self::new`].
532    pub fn new_blocking(
533        window: Arc<Window>,
534        settings: GpuRendererSettings,
535    ) -> Result<Self, GpuInitError> {
536        pollster::block_on(Self::new(window, settings))
537    }
538
539    /// Human-readable adapter description — name + backend +
540    /// device type. The demo host prints this in the title bar.
541    pub fn adapter_info(&self) -> &str {
542        &self.adapter_info
543    }
544
545    pub fn window(&self) -> &Window {
546        &self.window
547    }
548
549    /// Borrow the underlying wgpu device — hosts use this to build
550    /// chunk uploads (`GpuChunkResident::upload(gpu.device(), …)`).
551    pub fn device(&self) -> &wgpu::Device {
552        &self.device
553    }
554
555    /// Borrow the wgpu queue — hosts use this for read-back paths
556    /// (`GpuChunkResident::read_voxel_blocking(gpu.device(), gpu.queue(), …)`).
557    pub fn queue(&self) -> &wgpu::Queue {
558        &self.queue
559    }
560
561    /// GPU.8 — upload an equirectangular panorama as the scene's
562    /// sky texture. `rgba` is row-major, `width × height` pixels,
563    /// 4 bytes per pixel (R, G, B, A). The shader samples it with
564    /// `u = atan2(dir.x, dir.y) / (2π) + 0.5` (azimuth) and
565    /// `v = acos(-dir.z) / π` (elevation), matching standard
566    /// equirectangular layout (top of image = zenith for voxlap's
567    /// `+z = down` basis).
568    ///
569    /// # Panics
570    /// If `rgba.len() != (width * height * 4) as usize`.
571    pub fn set_sky_panorama(&mut self, rgba: &[u8], width: u32, height: u32) {
572        assert_eq!(
573            rgba.len(),
574            (width as usize) * (height as usize) * 4,
575            "set_sky_panorama: expected w*h*4 bytes, got {}",
576            rgba.len(),
577        );
578        let (tex, view) = create_sky_texture(&self.device, width, height, rgba);
579        // Upload pixel data via `queue.write_texture` so we don't
580        // have to map the buffer manually.
581        self.queue.write_texture(
582            wgpu::ImageCopyTexture {
583                texture: &tex,
584                mip_level: 0,
585                origin: wgpu::Origin3d::ZERO,
586                aspect: wgpu::TextureAspect::All,
587            },
588            rgba,
589            wgpu::ImageDataLayout {
590                offset: 0,
591                bytes_per_row: Some(width * 4),
592                rows_per_image: Some(height),
593            },
594            wgpu::Extent3d {
595                width,
596                height,
597                depth_or_array_layers: 1,
598            },
599        );
600        self.sky_texture = tex;
601        self.sky_view = view;
602    }
603
604    /// GPU.8 — set the fog blend. `color` is per-channel [0, 1];
605    /// `near`/`far` are world-space ray distances in voxel units.
606    /// Hits with `t < near` show their full colour; hits with
607    /// `t > far` show `color` exclusively; in between is a
608    /// smoothstep blend.
609    pub fn set_fog(&mut self, color: [f32; 3], near: f32, far: f32) {
610        self.fog_color = color;
611        self.fog_near = near;
612        self.fog_far = far.max(near + 1.0);
613    }
614
615    /// Re-configure the swapchain to a new physical size. Call from
616    /// `WindowEvent::Resized`. Drops the chunk-DDA storage texture
617    /// so [`Self::render_chunk`] rebuilds it at the new size.
618    pub fn resize(&mut self, width: u32, height: u32) {
619        if width == 0 || height == 0 {
620            return;
621        }
622        self.surface_config.width = width;
623        self.surface_config.height = height;
624        self.surface.configure(&self.device, &self.surface_config);
625        self.chunk_dda = None;
626        self.grid_dda = None;
627        self.scene_dda = None;
628    }
629
630    /// GPU.1 render: single render pass clearing the swapchain to a
631    /// slowly drifting colour, then presenting. Voxels arrive in
632    /// GPU.3+.
633    pub fn render(&mut self) {
634        let surf_tex = match self.surface.get_current_texture() {
635            Ok(t) => t,
636            Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
637                self.surface.configure(&self.device, &self.surface_config);
638                return;
639            }
640            Err(e) => {
641                eprintln!("roxlap-gpu surface error: {e:?}");
642                return;
643            }
644        };
645        let view = surf_tex
646            .texture
647            .create_view(&wgpu::TextureViewDescriptor::default());
648
649        // Slow colour drift so the user can tell the GPU path is
650        // actually presenting frames vs. e.g. a frozen window.
651        // Wrap at 2π/0.005 frames (~1257) so the cast stays exact.
652        let phase = f64::from(self.frame_count % 1257) * 0.005;
653        let [r, g, b] = self.clear_colour;
654        let drift = (phase.sin() * 0.04 + 0.04).clamp(0.0, 0.1);
655        let clear = wgpu::Color {
656            r: (r + drift).clamp(0.0, 1.0),
657            g: (g + drift * 0.5).clamp(0.0, 1.0),
658            b: (b + drift * 0.25).clamp(0.0, 1.0),
659            a: 1.0,
660        };
661
662        let mut encoder = self
663            .device
664            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
665                label: Some("roxlap-gpu encoder"),
666            });
667        {
668            let _rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
669                label: Some("roxlap-gpu clear"),
670                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
671                    view: &view,
672                    resolve_target: None,
673                    ops: wgpu::Operations {
674                        load: wgpu::LoadOp::Clear(clear),
675                        store: wgpu::StoreOp::Store,
676                    },
677                })],
678                depth_stencil_attachment: None,
679                timestamp_writes: None,
680                occlusion_query_set: None,
681            });
682        }
683        self.queue.submit(std::iter::once(encoder.finish()));
684        surf_tex.present();
685        self.frame_count = self.frame_count.wrapping_add(1);
686    }
687
688    /// GPU.3 single-chunk render. Dispatches `chunk_dda.wgsl`
689    /// against `resident`'s storage buffers, then blits the
690    /// low-res storage texture to the swapchain. `camera.position`
691    /// is in **chunk-local** voxel units (host translates from
692    /// world coords). `max_scan_dist` caps the per-pixel DDA loop —
693    /// scene-demo wires `+` / `-` through this each frame.
694    ///
695    /// # Panics
696    /// Internally `expect`s the chunk-DDA resources to be built —
697    /// they are constructed at the top of this function if missing.
698    /// Cannot fire in normal control flow.
699    pub fn render_chunk(
700        &mut self,
701        resident: &GpuChunkResident,
702        camera: &Camera,
703        max_scan_dist: u32,
704    ) {
705        let surf_tex = match self.surface.get_current_texture() {
706            Ok(t) => t,
707            Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
708                self.surface.configure(&self.device, &self.surface_config);
709                return;
710            }
711            Err(e) => {
712                eprintln!("roxlap-gpu surface error: {e:?}");
713                return;
714            }
715        };
716        let surf_view = surf_tex
717            .texture
718            .create_view(&wgpu::TextureViewDescriptor::default());
719
720        let surface_w = self.surface_config.width;
721        let surface_h = self.surface_config.height;
722        let surface_format = self.surface_config.format;
723
724        // Lazy-build chunk-DDA resources; rebuild when the swapchain
725        // grew or shrank.
726        let needs_build = match &self.chunk_dda {
727            Some(r) => r.storage_size != (surface_w, surface_h),
728            None => true,
729        };
730        if needs_build {
731            self.chunk_dda = Some(self.build_chunk_dda(surface_w, surface_h, surface_format));
732        }
733        let dda = self.chunk_dda.as_ref().expect("just built");
734
735        // Update uniforms.
736        let uniform = ChunkDdaUniform {
737            camera_pos: camera.position,
738            _pad0: 0.0,
739            camera_right: camera.right,
740            _pad1: 0.0,
741            camera_down: camera.down,
742            _pad2: 0.0,
743            camera_forward: camera.forward,
744            fov_y_rad: camera.fov_y_rad,
745            screen_size: [surface_w, surface_h],
746            vsid: resident.vsid,
747            max_scan_dist,
748        };
749        self.queue
750            .write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
751
752        // Per-frame DDA bind group — references the chunk's buffers
753        // so we rebuild every frame (the resident can change between
754        // calls).
755        let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
756            label: Some("roxlap-gpu chunk_dda.bg"),
757            layout: &dda.bgl_dda,
758            entries: &[
759                wgpu::BindGroupEntry {
760                    binding: 0,
761                    resource: dda.uniform_buf.as_entire_binding(),
762                },
763                wgpu::BindGroupEntry {
764                    binding: 1,
765                    resource: resident.occupancy.as_entire_binding(),
766                },
767                wgpu::BindGroupEntry {
768                    binding: 2,
769                    resource: resident.color_offsets.as_entire_binding(),
770                },
771                wgpu::BindGroupEntry {
772                    binding: 3,
773                    resource: resident.colors.as_entire_binding(),
774                },
775                wgpu::BindGroupEntry {
776                    binding: 4,
777                    resource: wgpu::BindingResource::TextureView(&dda.storage_view),
778                },
779            ],
780        });
781
782        let mut encoder = self
783            .device
784            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
785                label: Some("roxlap-gpu chunk encoder"),
786            });
787        {
788            let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
789                label: Some("roxlap-gpu chunk_dda compute"),
790                timestamp_writes: None,
791            });
792            cpass.set_pipeline(&dda.pipeline_dda);
793            cpass.set_bind_group(0, &dda_bg, &[]);
794            cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
795        }
796        {
797            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
798                label: Some("roxlap-gpu chunk_dda blit"),
799                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
800                    view: &surf_view,
801                    resolve_target: None,
802                    ops: wgpu::Operations {
803                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
804                        store: wgpu::StoreOp::Store,
805                    },
806                })],
807                depth_stencil_attachment: None,
808                timestamp_writes: None,
809                occlusion_query_set: None,
810            });
811            rpass.set_pipeline(&dda.pipeline_blit);
812            rpass.set_bind_group(0, &dda.blit_bg, &[]);
813            rpass.draw(0..3, 0..1);
814        }
815        self.queue.submit(std::iter::once(encoder.finish()));
816        surf_tex.present();
817        self.frame_count = self.frame_count.wrapping_add(1);
818    }
819
820    fn build_chunk_dda(
821        &self,
822        width: u32,
823        height: u32,
824        surface_format: wgpu::TextureFormat,
825    ) -> ChunkDdaResources {
826        let storage_tex = self.device.create_texture(&wgpu::TextureDescriptor {
827            label: Some("roxlap-gpu chunk_dda.storage"),
828            size: wgpu::Extent3d {
829                width,
830                height,
831                depth_or_array_layers: 1,
832            },
833            mip_level_count: 1,
834            sample_count: 1,
835            dimension: wgpu::TextureDimension::D2,
836            format: wgpu::TextureFormat::Rgba8Unorm,
837            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
838            view_formats: &[],
839        });
840        let storage_view = storage_tex.create_view(&wgpu::TextureViewDescriptor::default());
841
842        let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
843            label: Some("roxlap-gpu chunk_dda.uniform"),
844            size: std::mem::size_of::<ChunkDdaUniform>() as u64,
845            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
846            mapped_at_creation: false,
847        });
848
849        let dda_shader = self
850            .device
851            .create_shader_module(wgpu::ShaderModuleDescriptor {
852                label: Some("chunk_dda.wgsl"),
853                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/chunk_dda.wgsl").into()),
854            });
855        let bgl_dda = self
856            .device
857            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
858                label: Some("roxlap-gpu chunk_dda.bgl"),
859                entries: &[
860                    bgl_uniform_entry(0),
861                    bgl_storage_entry(1, true),
862                    bgl_storage_entry(2, true),
863                    bgl_storage_entry(3, true),
864                    wgpu::BindGroupLayoutEntry {
865                        binding: 4,
866                        visibility: wgpu::ShaderStages::COMPUTE,
867                        ty: wgpu::BindingType::StorageTexture {
868                            access: wgpu::StorageTextureAccess::WriteOnly,
869                            format: wgpu::TextureFormat::Rgba8Unorm,
870                            view_dimension: wgpu::TextureViewDimension::D2,
871                        },
872                        count: None,
873                    },
874                ],
875            });
876        let dda_pl = self
877            .device
878            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
879                label: Some("roxlap-gpu chunk_dda.layout"),
880                bind_group_layouts: &[&bgl_dda],
881                push_constant_ranges: &[],
882            });
883        let pipeline_dda = self
884            .device
885            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
886                label: Some("roxlap-gpu chunk_dda.pipeline"),
887                layout: Some(&dda_pl),
888                module: &dda_shader,
889                entry_point: "render_chunk",
890                compilation_options: wgpu::PipelineCompilationOptions::default(),
891                cache: None,
892            });
893
894        // Fullscreen-triangle blit upscales the storage texture into
895        // the swapchain. Nearest filter keeps the retro pixel look.
896        let blit_shader = self
897            .device
898            .create_shader_module(wgpu::ShaderModuleDescriptor {
899                label: Some("blit.wgsl"),
900                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
901            });
902        let bgl_blit = self
903            .device
904            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
905                label: Some("roxlap-gpu chunk_dda.blit_bgl"),
906                entries: &[
907                    wgpu::BindGroupLayoutEntry {
908                        binding: 0,
909                        visibility: wgpu::ShaderStages::FRAGMENT,
910                        ty: wgpu::BindingType::Texture {
911                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
912                            view_dimension: wgpu::TextureViewDimension::D2,
913                            multisampled: false,
914                        },
915                        count: None,
916                    },
917                    wgpu::BindGroupLayoutEntry {
918                        binding: 1,
919                        visibility: wgpu::ShaderStages::FRAGMENT,
920                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
921                        count: None,
922                    },
923                ],
924            });
925        let blit_pl = self
926            .device
927            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
928                label: Some("roxlap-gpu chunk_dda.blit_layout"),
929                bind_group_layouts: &[&bgl_blit],
930                push_constant_ranges: &[],
931            });
932        let pipeline_blit = self
933            .device
934            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
935                label: Some("roxlap-gpu chunk_dda.blit_pipeline"),
936                layout: Some(&blit_pl),
937                vertex: wgpu::VertexState {
938                    module: &blit_shader,
939                    entry_point: "vs_main",
940                    compilation_options: wgpu::PipelineCompilationOptions::default(),
941                    buffers: &[],
942                },
943                fragment: Some(wgpu::FragmentState {
944                    module: &blit_shader,
945                    entry_point: "fs_main",
946                    compilation_options: wgpu::PipelineCompilationOptions::default(),
947                    targets: &[Some(wgpu::ColorTargetState {
948                        format: surface_format,
949                        blend: None,
950                        write_mask: wgpu::ColorWrites::ALL,
951                    })],
952                }),
953                primitive: wgpu::PrimitiveState::default(),
954                depth_stencil: None,
955                multisample: wgpu::MultisampleState::default(),
956                multiview: None,
957                cache: None,
958            });
959        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
960            label: Some("roxlap-gpu chunk_dda.blit_sampler"),
961            address_mode_u: wgpu::AddressMode::ClampToEdge,
962            address_mode_v: wgpu::AddressMode::ClampToEdge,
963            address_mode_w: wgpu::AddressMode::ClampToEdge,
964            mag_filter: wgpu::FilterMode::Nearest,
965            min_filter: wgpu::FilterMode::Nearest,
966            mipmap_filter: wgpu::FilterMode::Nearest,
967            ..Default::default()
968        });
969        let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
970            label: Some("roxlap-gpu chunk_dda.blit_bg"),
971            layout: &bgl_blit,
972            entries: &[
973                wgpu::BindGroupEntry {
974                    binding: 0,
975                    resource: wgpu::BindingResource::TextureView(&storage_view),
976                },
977                wgpu::BindGroupEntry {
978                    binding: 1,
979                    resource: wgpu::BindingResource::Sampler(&sampler),
980                },
981            ],
982        });
983
984        ChunkDdaResources {
985            storage_size: (width, height),
986            storage_view,
987            uniform_buf,
988            bgl_dda,
989            pipeline_dda,
990            blit_bg,
991            pipeline_blit,
992            _sampler: sampler,
993        }
994    }
995
996    /// GPU.4 render — outer DDA over chunk indices + inner DDA into
997    /// non-empty chunks. `camera.position` is in **grid-local**
998    /// voxel units. `max_outer_steps` caps how many chunks the
999    /// outer DDA may traverse per ray (scene-demo wires `+ / -`
1000    /// through this).
1001    ///
1002    /// # Panics
1003    /// Internally `expect`s the grid-DDA resources to be built;
1004    /// they are constructed at the top of this function if missing.
1005    pub fn render_grid(&mut self, grid: &GpuGridResident, camera: &Camera, max_outer_steps: u32) {
1006        let surf_tex = match self.surface.get_current_texture() {
1007            Ok(t) => t,
1008            Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
1009                self.surface.configure(&self.device, &self.surface_config);
1010                return;
1011            }
1012            Err(e) => {
1013                eprintln!("roxlap-gpu surface error: {e:?}");
1014                return;
1015            }
1016        };
1017        let surf_view = surf_tex
1018            .texture
1019            .create_view(&wgpu::TextureViewDescriptor::default());
1020
1021        let surface_w = self.surface_config.width;
1022        let surface_h = self.surface_config.height;
1023        let surface_format = self.surface_config.format;
1024
1025        let needs_build = match &self.grid_dda {
1026            Some(r) => r.storage_size != (surface_w, surface_h),
1027            None => true,
1028        };
1029        if needs_build {
1030            self.grid_dda = Some(self.build_grid_dda(surface_w, surface_h, surface_format));
1031        }
1032        let dda = self.grid_dda.as_ref().expect("just built");
1033
1034        let uniform = GridDdaUniform {
1035            camera_pos: camera.position,
1036            _pad0: 0.0,
1037            camera_right: camera.right,
1038            _pad1: 0.0,
1039            camera_down: camera.down,
1040            _pad2: 0.0,
1041            camera_forward: camera.forward,
1042            fov_y_rad: camera.fov_y_rad,
1043            screen_size: [surface_w, surface_h],
1044            vsid: grid.vsid,
1045            max_outer_steps,
1046            chunks_dims: grid.chunks_dims,
1047            _pad3: 0,
1048            origin_chunk: grid.origin_chunk,
1049            _pad4: 0,
1050        };
1051        self.queue
1052            .write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
1053
1054        let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1055            label: Some("roxlap-gpu grid_dda.bg"),
1056            layout: &dda.bgl_dda,
1057            entries: &[
1058                wgpu::BindGroupEntry {
1059                    binding: 0,
1060                    resource: dda.uniform_buf.as_entire_binding(),
1061                },
1062                wgpu::BindGroupEntry {
1063                    binding: 1,
1064                    resource: grid.occupancy.as_entire_binding(),
1065                },
1066                wgpu::BindGroupEntry {
1067                    binding: 2,
1068                    resource: grid.color_offsets.as_entire_binding(),
1069                },
1070                wgpu::BindGroupEntry {
1071                    binding: 3,
1072                    resource: grid.colors.as_entire_binding(),
1073                },
1074                wgpu::BindGroupEntry {
1075                    binding: 4,
1076                    resource: grid.chunk_colors_base.as_entire_binding(),
1077                },
1078                wgpu::BindGroupEntry {
1079                    binding: 5,
1080                    resource: grid.chunk_occupancy.as_entire_binding(),
1081                },
1082                wgpu::BindGroupEntry {
1083                    binding: 6,
1084                    resource: wgpu::BindingResource::TextureView(&dda.storage_view),
1085                },
1086            ],
1087        });
1088
1089        let mut encoder = self
1090            .device
1091            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1092                label: Some("roxlap-gpu grid encoder"),
1093            });
1094        {
1095            let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1096                label: Some("roxlap-gpu grid_dda compute"),
1097                timestamp_writes: None,
1098            });
1099            cpass.set_pipeline(&dda.pipeline_dda);
1100            cpass.set_bind_group(0, &dda_bg, &[]);
1101            cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
1102        }
1103        {
1104            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1105                label: Some("roxlap-gpu grid_dda blit"),
1106                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1107                    view: &surf_view,
1108                    resolve_target: None,
1109                    ops: wgpu::Operations {
1110                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1111                        store: wgpu::StoreOp::Store,
1112                    },
1113                })],
1114                depth_stencil_attachment: None,
1115                timestamp_writes: None,
1116                occlusion_query_set: None,
1117            });
1118            rpass.set_pipeline(&dda.pipeline_blit);
1119            rpass.set_bind_group(0, &dda.blit_bg, &[]);
1120            rpass.draw(0..3, 0..1);
1121        }
1122        self.queue.submit(std::iter::once(encoder.finish()));
1123        surf_tex.present();
1124        self.frame_count = self.frame_count.wrapping_add(1);
1125    }
1126
1127    fn build_grid_dda(
1128        &self,
1129        width: u32,
1130        height: u32,
1131        surface_format: wgpu::TextureFormat,
1132    ) -> GridDdaResources {
1133        let storage_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1134            label: Some("roxlap-gpu grid_dda.storage"),
1135            size: wgpu::Extent3d {
1136                width,
1137                height,
1138                depth_or_array_layers: 1,
1139            },
1140            mip_level_count: 1,
1141            sample_count: 1,
1142            dimension: wgpu::TextureDimension::D2,
1143            format: wgpu::TextureFormat::Rgba8Unorm,
1144            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
1145            view_formats: &[],
1146        });
1147        let storage_view = storage_tex.create_view(&wgpu::TextureViewDescriptor::default());
1148
1149        let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1150            label: Some("roxlap-gpu grid_dda.uniform"),
1151            size: std::mem::size_of::<GridDdaUniform>() as u64,
1152            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1153            mapped_at_creation: false,
1154        });
1155
1156        let dda_shader = self
1157            .device
1158            .create_shader_module(wgpu::ShaderModuleDescriptor {
1159                label: Some("grid_dda.wgsl"),
1160                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/grid_dda.wgsl").into()),
1161            });
1162        let bgl_dda = self
1163            .device
1164            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1165                label: Some("roxlap-gpu grid_dda.bgl"),
1166                entries: &[
1167                    bgl_uniform_entry(0),
1168                    bgl_storage_entry(1, true),
1169                    bgl_storage_entry(2, true),
1170                    bgl_storage_entry(3, true),
1171                    bgl_storage_entry(4, true),
1172                    bgl_storage_entry(5, true),
1173                    wgpu::BindGroupLayoutEntry {
1174                        binding: 6,
1175                        visibility: wgpu::ShaderStages::COMPUTE,
1176                        ty: wgpu::BindingType::StorageTexture {
1177                            access: wgpu::StorageTextureAccess::WriteOnly,
1178                            format: wgpu::TextureFormat::Rgba8Unorm,
1179                            view_dimension: wgpu::TextureViewDimension::D2,
1180                        },
1181                        count: None,
1182                    },
1183                ],
1184            });
1185        let dda_pl = self
1186            .device
1187            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1188                label: Some("roxlap-gpu grid_dda.layout"),
1189                bind_group_layouts: &[&bgl_dda],
1190                push_constant_ranges: &[],
1191            });
1192        let pipeline_dda = self
1193            .device
1194            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1195                label: Some("roxlap-gpu grid_dda.pipeline"),
1196                layout: Some(&dda_pl),
1197                module: &dda_shader,
1198                entry_point: "render_grid",
1199                compilation_options: wgpu::PipelineCompilationOptions::default(),
1200                cache: None,
1201            });
1202
1203        let blit_shader = self
1204            .device
1205            .create_shader_module(wgpu::ShaderModuleDescriptor {
1206                label: Some("blit.wgsl"),
1207                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
1208            });
1209        let bgl_blit = self
1210            .device
1211            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1212                label: Some("roxlap-gpu grid_dda.blit_bgl"),
1213                entries: &[
1214                    wgpu::BindGroupLayoutEntry {
1215                        binding: 0,
1216                        visibility: wgpu::ShaderStages::FRAGMENT,
1217                        ty: wgpu::BindingType::Texture {
1218                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
1219                            view_dimension: wgpu::TextureViewDimension::D2,
1220                            multisampled: false,
1221                        },
1222                        count: None,
1223                    },
1224                    wgpu::BindGroupLayoutEntry {
1225                        binding: 1,
1226                        visibility: wgpu::ShaderStages::FRAGMENT,
1227                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
1228                        count: None,
1229                    },
1230                ],
1231            });
1232        let blit_pl = self
1233            .device
1234            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1235                label: Some("roxlap-gpu grid_dda.blit_layout"),
1236                bind_group_layouts: &[&bgl_blit],
1237                push_constant_ranges: &[],
1238            });
1239        let pipeline_blit = self
1240            .device
1241            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1242                label: Some("roxlap-gpu grid_dda.blit_pipeline"),
1243                layout: Some(&blit_pl),
1244                vertex: wgpu::VertexState {
1245                    module: &blit_shader,
1246                    entry_point: "vs_main",
1247                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1248                    buffers: &[],
1249                },
1250                fragment: Some(wgpu::FragmentState {
1251                    module: &blit_shader,
1252                    entry_point: "fs_main",
1253                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1254                    targets: &[Some(wgpu::ColorTargetState {
1255                        format: surface_format,
1256                        blend: None,
1257                        write_mask: wgpu::ColorWrites::ALL,
1258                    })],
1259                }),
1260                primitive: wgpu::PrimitiveState::default(),
1261                depth_stencil: None,
1262                multisample: wgpu::MultisampleState::default(),
1263                multiview: None,
1264                cache: None,
1265            });
1266        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1267            label: Some("roxlap-gpu grid_dda.blit_sampler"),
1268            address_mode_u: wgpu::AddressMode::ClampToEdge,
1269            address_mode_v: wgpu::AddressMode::ClampToEdge,
1270            address_mode_w: wgpu::AddressMode::ClampToEdge,
1271            mag_filter: wgpu::FilterMode::Nearest,
1272            min_filter: wgpu::FilterMode::Nearest,
1273            mipmap_filter: wgpu::FilterMode::Nearest,
1274            ..Default::default()
1275        });
1276        let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1277            label: Some("roxlap-gpu grid_dda.blit_bg"),
1278            layout: &bgl_blit,
1279            entries: &[
1280                wgpu::BindGroupEntry {
1281                    binding: 0,
1282                    resource: wgpu::BindingResource::TextureView(&storage_view),
1283                },
1284                wgpu::BindGroupEntry {
1285                    binding: 1,
1286                    resource: wgpu::BindingResource::Sampler(&sampler),
1287                },
1288            ],
1289        });
1290
1291        GridDdaResources {
1292            storage_size: (width, height),
1293            storage_view,
1294            uniform_buf,
1295            bgl_dda,
1296            pipeline_dda,
1297            blit_bg,
1298            pipeline_blit,
1299            _sampler: sampler,
1300        }
1301    }
1302
1303    /// GPU.5 render — multi-grid scene marcher. `cameras[i]` is the
1304    /// world camera transformed into grid `i`'s local frame
1305    /// (caller-supplied; see scene-demo's `redraw_gpu` for the
1306    /// glam-based transform). `fov_y_rad` is the shared vertical
1307    /// FOV; `max_outer_steps` caps per-ray chunk-DDA work for each
1308    /// grid.
1309    ///
1310    /// # Panics
1311    /// If `cameras.len() != scene.grid_count` or
1312    /// `scene.grid_count > MAX_SCENE_GRIDS`.
1313    pub fn render_scene(
1314        &mut self,
1315        scene: &GpuSceneResident,
1316        cameras: &[Camera],
1317        fov_y_rad: f32,
1318        max_outer_steps: u32,
1319    ) {
1320        assert_eq!(
1321            cameras.len(),
1322            scene.grid_count as usize,
1323            "render_scene: {} cameras supplied, scene has {} grids",
1324            cameras.len(),
1325            scene.grid_count,
1326        );
1327        assert!(
1328            scene.grid_count as usize <= SCENE_MAX_GRIDS,
1329            "render_scene: scene has {} grids, shader supports {}",
1330            scene.grid_count,
1331            SCENE_MAX_GRIDS,
1332        );
1333
1334        let surf_tex = match self.surface.get_current_texture() {
1335            Ok(t) => t,
1336            Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => {
1337                self.surface.configure(&self.device, &self.surface_config);
1338                return;
1339            }
1340            Err(e) => {
1341                eprintln!("roxlap-gpu surface error: {e:?}");
1342                return;
1343            }
1344        };
1345        let surf_view = surf_tex
1346            .texture
1347            .create_view(&wgpu::TextureViewDescriptor::default());
1348
1349        let surface_w = self.surface_config.width;
1350        let surface_h = self.surface_config.height;
1351        let surface_format = self.surface_config.format;
1352
1353        let needs_build = match &self.scene_dda {
1354            Some(r) => r.storage_size != (surface_w, surface_h),
1355            None => true,
1356        };
1357        if needs_build {
1358            self.scene_dda = Some(self.build_scene_dda(surface_w, surface_h, surface_format));
1359        }
1360        // GPU.9 — materialise the sprite pipeline the first frame
1361        // sprites are present (before the immutable `dda` borrow).
1362        // GPU.10.0 — build the model-DDA pipeline the first frame a
1363        // sprite registry is present.
1364        if self.sprite_registry.is_some() && self.sprite_model_dda.is_none() {
1365            self.sprite_model_dda = Some(self.build_sprite_model_dda());
1366        }
1367        // GPU.10.3 — frustum-cull + screen-tile-bin the sprite instances
1368        // (needs &mut self for buffer growth, so before the immutable
1369        // scene_dda borrow). Captures (visible_count, tiles_x); None when
1370        // nothing is in view.
1371        let sprite_pass: Option<(u32, u32)> = if let Some(reg) = self.sprite_registry.as_mut() {
1372            if !cameras.is_empty() && reg.instance_capacity > 0 {
1373                let cam = &cameras[0];
1374                #[allow(clippy::cast_precision_loss)]
1375                let aspect = surface_w as f32 / surface_h as f32;
1376                let half_h = (fov_y_rad * 0.5).tan();
1377                let frustum = sprite_model::ViewFrustum {
1378                    pos: cam.position,
1379                    right: cam.right,
1380                    down: cam.down,
1381                    forward: cam.forward,
1382                    half_w: half_h * aspect,
1383                    half_h,
1384                    far: 1.0e9,
1385                };
1386                let (visible, tiles_x, _tiles_y) = reg.cull_bin_upload(
1387                    &self.device,
1388                    &self.queue,
1389                    &frustum,
1390                    surface_w,
1391                    surface_h,
1392                    SPRITE_TILE_SIZE,
1393                    self.sprite_lod_px,
1394                );
1395                (visible > 0).then_some((visible, tiles_x))
1396            } else {
1397                None
1398            }
1399        } else {
1400            None
1401        };
1402        let dda = self.scene_dda.as_ref().expect("just built");
1403
1404        // Pack per-grid cameras.
1405        let mut cam_array = [SceneDdaPerGridCamera::zeroed(); SCENE_MAX_GRIDS];
1406        for (i, cam) in cameras.iter().enumerate() {
1407            cam_array[i] = SceneDdaPerGridCamera {
1408                pos: cam.position,
1409                _pad0: 0.0,
1410                right: cam.right,
1411                _pad1: 0.0,
1412                down: cam.down,
1413                _pad2: 0.0,
1414                forward: cam.forward,
1415                _pad3: 0.0,
1416            };
1417        }
1418        let uniform = SceneDdaUniform {
1419            fov_y_rad,
1420            grid_count: scene.grid_count,
1421            max_outer_steps,
1422            _pad0: 0,
1423            screen_size: [surface_w, surface_h],
1424            _pad1: [0; 2],
1425            cameras: cam_array,
1426            fog_color: [
1427                self.fog_color[0],
1428                self.fog_color[1],
1429                self.fog_color[2],
1430                self.fog_near,
1431            ],
1432            fog_far: self.fog_far,
1433            write_depth: u32::from(self.sprite_registry.is_some()),
1434            occ_page_words: scene.occupancy_page_words,
1435            occ_num_pages: scene.occupancy_num_pages,
1436            mip_scan_dist: self.scene_mip_scan_dist,
1437            _pad2: 0,
1438            _pad3: 0,
1439            _pad4: 0,
1440        };
1441        self.queue
1442            .write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
1443
1444        let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1445            label: Some("roxlap-gpu scene_dda.bg"),
1446            layout: &dda.bgl_dda,
1447            entries: &[
1448                wgpu::BindGroupEntry {
1449                    binding: 0,
1450                    resource: dda.uniform_buf.as_entire_binding(),
1451                },
1452                // Occupancy page 0 at binding 1; pages 1..MAX_OCC_PAGES
1453                // at bindings 12.. (see GPU.X occupancy paging).
1454                wgpu::BindGroupEntry {
1455                    binding: 1,
1456                    resource: scene.occupancy_pages[0].as_entire_binding(),
1457                },
1458                wgpu::BindGroupEntry {
1459                    binding: 2,
1460                    resource: scene.all_color_offsets.as_entire_binding(),
1461                },
1462                wgpu::BindGroupEntry {
1463                    binding: 3,
1464                    resource: scene.all_colors.as_entire_binding(),
1465                },
1466                wgpu::BindGroupEntry {
1467                    binding: 4,
1468                    resource: scene.all_chunk_colors_base.as_entire_binding(),
1469                },
1470                wgpu::BindGroupEntry {
1471                    binding: 5,
1472                    resource: scene.all_chunk_occupancy.as_entire_binding(),
1473                },
1474                wgpu::BindGroupEntry {
1475                    binding: 6,
1476                    resource: scene.grid_static_meta.as_entire_binding(),
1477                },
1478                wgpu::BindGroupEntry {
1479                    binding: 7,
1480                    resource: scene.all_slot_chunk_idx.as_entire_binding(),
1481                },
1482                wgpu::BindGroupEntry {
1483                    binding: 8,
1484                    resource: wgpu::BindingResource::TextureView(&dda.storage_view),
1485                },
1486                wgpu::BindGroupEntry {
1487                    binding: 9,
1488                    resource: wgpu::BindingResource::TextureView(&self.sky_view),
1489                },
1490                wgpu::BindGroupEntry {
1491                    binding: 10,
1492                    resource: wgpu::BindingResource::Sampler(&self.sky_sampler),
1493                },
1494                wgpu::BindGroupEntry {
1495                    binding: 11,
1496                    resource: dda.depth_buffer.as_entire_binding(),
1497                },
1498                wgpu::BindGroupEntry {
1499                    binding: 12,
1500                    resource: scene.occupancy_pages[1].as_entire_binding(),
1501                },
1502                wgpu::BindGroupEntry {
1503                    binding: 13,
1504                    resource: scene.occupancy_pages[2].as_entire_binding(),
1505                },
1506                wgpu::BindGroupEntry {
1507                    binding: 14,
1508                    resource: scene.occupancy_pages[3].as_entire_binding(),
1509                },
1510            ],
1511        });
1512
1513        // GPU.9 — when sprites are present, build both splatter bind
1514        // groups up front (the splat pass writes the key buffer; the
1515        // resolve pass reads keys + scene depth and writes colour).
1516        // GPU.10.3 — model-DDA bind group + per-frame uniform, using the
1517        // cull/bin results captured above. Per-model + per-instance data
1518        // + the tile lists live in the registry buffers.
1519        let sprite_model_bg = match (&self.sprite_model_dda, &self.sprite_registry, sprite_pass) {
1520            (Some(smd), Some(reg), Some((visible, tiles_x))) => {
1521                let cam = &cameras[0];
1522                let uni = SpriteModelUniform {
1523                    cam_pos: cam.position,
1524                    _p0: 0.0,
1525                    cam_right: cam.right,
1526                    _p1: 0.0,
1527                    cam_down: cam.down,
1528                    _p2: 0.0,
1529                    cam_forward: cam.forward,
1530                    _p3: 0.0,
1531                    fog_color: [
1532                        self.fog_color[0],
1533                        self.fog_color[1],
1534                        self.fog_color[2],
1535                        self.fog_near,
1536                    ],
1537                    screen_size: [surface_w, surface_h],
1538                    instance_count: visible,
1539                    fog_far: self.fog_far,
1540                    fov_y_rad,
1541                    tiles_x,
1542                    tile_size: SPRITE_TILE_SIZE,
1543                    _p6: 0.0,
1544                };
1545                self.queue
1546                    .write_buffer(&smd.uniform_buf, 0, bytemuck::bytes_of(&uni));
1547                Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1548                    label: Some("roxlap-gpu sprite_model_dda.bg"),
1549                    layout: &smd.bgl,
1550                    entries: &[
1551                        wgpu::BindGroupEntry {
1552                            binding: 0,
1553                            resource: smd.uniform_buf.as_entire_binding(),
1554                        },
1555                        wgpu::BindGroupEntry {
1556                            binding: 1,
1557                            resource: reg.occupancy.as_entire_binding(),
1558                        },
1559                        wgpu::BindGroupEntry {
1560                            binding: 2,
1561                            resource: reg.colors.as_entire_binding(),
1562                        },
1563                        wgpu::BindGroupEntry {
1564                            binding: 3,
1565                            resource: reg.color_offsets.as_entire_binding(),
1566                        },
1567                        wgpu::BindGroupEntry {
1568                            binding: 4,
1569                            resource: reg.model_meta.as_entire_binding(),
1570                        },
1571                        wgpu::BindGroupEntry {
1572                            binding: 5,
1573                            resource: reg.instances.as_entire_binding(),
1574                        },
1575                        wgpu::BindGroupEntry {
1576                            binding: 6,
1577                            resource: dda.depth_buffer.as_entire_binding(),
1578                        },
1579                        wgpu::BindGroupEntry {
1580                            binding: 7,
1581                            resource: wgpu::BindingResource::TextureView(&dda.storage_view),
1582                        },
1583                        wgpu::BindGroupEntry {
1584                            binding: 8,
1585                            resource: reg.tile_ranges.as_entire_binding(),
1586                        },
1587                        wgpu::BindGroupEntry {
1588                            binding: 9,
1589                            resource: reg.tile_instances.as_entire_binding(),
1590                        },
1591                    ],
1592                }))
1593            }
1594            _ => None,
1595        };
1596
1597        let mut encoder = self
1598            .device
1599            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1600                label: Some("roxlap-gpu scene encoder"),
1601            });
1602        {
1603            let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1604                label: Some("roxlap-gpu scene_dda compute"),
1605                timestamp_writes: None,
1606            });
1607            cpass.set_pipeline(&dda.pipeline_dda);
1608            cpass.set_bind_group(0, &dda_bg, &[]);
1609            cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
1610        }
1611        // GPU.10 — sprite model-DDA pass: one thread per pixel marches
1612        // the tile's instances + composites against scene depth, after
1613        // the scene pass wrote the depth buffer and before the blit.
1614        if let (Some(smd), Some(bg)) = (&self.sprite_model_dda, &sprite_model_bg) {
1615            let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1616                label: Some("roxlap-gpu sprite_model_dda"),
1617                timestamp_writes: None,
1618            });
1619            cpass.set_pipeline(&smd.pipeline);
1620            cpass.set_bind_group(0, bg, &[]);
1621            cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
1622        }
1623        {
1624            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1625                label: Some("roxlap-gpu scene_dda blit"),
1626                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1627                    view: &surf_view,
1628                    resolve_target: None,
1629                    ops: wgpu::Operations {
1630                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1631                        store: wgpu::StoreOp::Store,
1632                    },
1633                })],
1634                depth_stencil_attachment: None,
1635                timestamp_writes: None,
1636                occlusion_query_set: None,
1637            });
1638            rpass.set_pipeline(&dda.pipeline_blit);
1639            rpass.set_bind_group(0, &dda.blit_bg, &[]);
1640            rpass.draw(0..3, 0..1);
1641        }
1642        self.queue.submit(std::iter::once(encoder.finish()));
1643        surf_tex.present();
1644        self.frame_count = self.frame_count.wrapping_add(1);
1645    }
1646
1647    fn build_scene_dda(
1648        &self,
1649        width: u32,
1650        height: u32,
1651        surface_format: wgpu::TextureFormat,
1652    ) -> SceneDdaResources {
1653        let storage_tex = self.device.create_texture(&wgpu::TextureDescriptor {
1654            label: Some("roxlap-gpu scene_dda.storage"),
1655            size: wgpu::Extent3d {
1656                width,
1657                height,
1658                depth_or_array_layers: 1,
1659            },
1660            mip_level_count: 1,
1661            sample_count: 1,
1662            dimension: wgpu::TextureDimension::D2,
1663            format: wgpu::TextureFormat::Rgba8Unorm,
1664            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
1665            view_formats: &[],
1666        });
1667        let storage_view = storage_tex.create_view(&wgpu::TextureViewDescriptor::default());
1668
1669        let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1670            label: Some("roxlap-gpu scene_dda.uniform"),
1671            size: std::mem::size_of::<SceneDdaUniform>() as u64,
1672            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1673            mapped_at_creation: false,
1674        });
1675
1676        // GPU.9 — per-pixel world-t depth (f32 bits as u32). Sized to
1677        // the storage texture; written by the scene pass when sprites
1678        // are active, read+tested by the sprite splatter.
1679        let depth_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1680            label: Some("roxlap-gpu scene_dda.depth"),
1681            size: u64::from(width) * u64::from(height) * 4,
1682            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1683            mapped_at_creation: false,
1684        });
1685        let dda_shader = self
1686            .device
1687            .create_shader_module(wgpu::ShaderModuleDescriptor {
1688                label: Some("scene_dda.wgsl"),
1689                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/scene_dda.wgsl").into()),
1690            });
1691        let bgl_dda = self
1692            .device
1693            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1694                label: Some("roxlap-gpu scene_dda.bgl"),
1695                entries: &[
1696                    bgl_uniform_entry(0),
1697                    bgl_storage_entry(1, true),
1698                    bgl_storage_entry(2, true),
1699                    bgl_storage_entry(3, true),
1700                    bgl_storage_entry(4, true),
1701                    bgl_storage_entry(5, true),
1702                    bgl_storage_entry(6, true),
1703                    bgl_storage_entry(7, true),
1704                    wgpu::BindGroupLayoutEntry {
1705                        binding: 8,
1706                        visibility: wgpu::ShaderStages::COMPUTE,
1707                        ty: wgpu::BindingType::StorageTexture {
1708                            access: wgpu::StorageTextureAccess::WriteOnly,
1709                            format: wgpu::TextureFormat::Rgba8Unorm,
1710                            view_dimension: wgpu::TextureViewDimension::D2,
1711                        },
1712                        count: None,
1713                    },
1714                    // GPU.8 sky panorama + sampler.
1715                    wgpu::BindGroupLayoutEntry {
1716                        binding: 9,
1717                        visibility: wgpu::ShaderStages::COMPUTE,
1718                        ty: wgpu::BindingType::Texture {
1719                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1720                            view_dimension: wgpu::TextureViewDimension::D2,
1721                            multisampled: false,
1722                        },
1723                        count: None,
1724                    },
1725                    wgpu::BindGroupLayoutEntry {
1726                        binding: 10,
1727                        visibility: wgpu::ShaderStages::COMPUTE,
1728                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1729                        count: None,
1730                    },
1731                    // GPU.9 — read-write per-pixel depth buffer.
1732                    bgl_storage_entry(11, false),
1733                    // Occupancy pages 1..MAX_OCC_PAGES (page 0 is
1734                    // binding 1). Unused pages bind a dummy buffer.
1735                    bgl_storage_entry(12, true),
1736                    bgl_storage_entry(13, true),
1737                    bgl_storage_entry(14, true),
1738                ],
1739            });
1740        let dda_pl = self
1741            .device
1742            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1743                label: Some("roxlap-gpu scene_dda.layout"),
1744                bind_group_layouts: &[&bgl_dda],
1745                push_constant_ranges: &[],
1746            });
1747        let pipeline_dda = self
1748            .device
1749            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1750                label: Some("roxlap-gpu scene_dda.pipeline"),
1751                layout: Some(&dda_pl),
1752                module: &dda_shader,
1753                entry_point: "render_scene",
1754                compilation_options: wgpu::PipelineCompilationOptions::default(),
1755                cache: None,
1756            });
1757
1758        let blit_shader = self
1759            .device
1760            .create_shader_module(wgpu::ShaderModuleDescriptor {
1761                label: Some("blit.wgsl"),
1762                source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
1763            });
1764        let bgl_blit = self
1765            .device
1766            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1767                label: Some("roxlap-gpu scene_dda.blit_bgl"),
1768                entries: &[
1769                    wgpu::BindGroupLayoutEntry {
1770                        binding: 0,
1771                        visibility: wgpu::ShaderStages::FRAGMENT,
1772                        ty: wgpu::BindingType::Texture {
1773                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
1774                            view_dimension: wgpu::TextureViewDimension::D2,
1775                            multisampled: false,
1776                        },
1777                        count: None,
1778                    },
1779                    wgpu::BindGroupLayoutEntry {
1780                        binding: 1,
1781                        visibility: wgpu::ShaderStages::FRAGMENT,
1782                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
1783                        count: None,
1784                    },
1785                ],
1786            });
1787        let blit_pl = self
1788            .device
1789            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1790                label: Some("roxlap-gpu scene_dda.blit_layout"),
1791                bind_group_layouts: &[&bgl_blit],
1792                push_constant_ranges: &[],
1793            });
1794        let pipeline_blit = self
1795            .device
1796            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1797                label: Some("roxlap-gpu scene_dda.blit_pipeline"),
1798                layout: Some(&blit_pl),
1799                vertex: wgpu::VertexState {
1800                    module: &blit_shader,
1801                    entry_point: "vs_main",
1802                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1803                    buffers: &[],
1804                },
1805                fragment: Some(wgpu::FragmentState {
1806                    module: &blit_shader,
1807                    entry_point: "fs_main",
1808                    compilation_options: wgpu::PipelineCompilationOptions::default(),
1809                    targets: &[Some(wgpu::ColorTargetState {
1810                        format: surface_format,
1811                        blend: None,
1812                        write_mask: wgpu::ColorWrites::ALL,
1813                    })],
1814                }),
1815                primitive: wgpu::PrimitiveState::default(),
1816                depth_stencil: None,
1817                multisample: wgpu::MultisampleState::default(),
1818                multiview: None,
1819                cache: None,
1820            });
1821        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
1822            label: Some("roxlap-gpu scene_dda.blit_sampler"),
1823            address_mode_u: wgpu::AddressMode::ClampToEdge,
1824            address_mode_v: wgpu::AddressMode::ClampToEdge,
1825            address_mode_w: wgpu::AddressMode::ClampToEdge,
1826            mag_filter: wgpu::FilterMode::Nearest,
1827            min_filter: wgpu::FilterMode::Nearest,
1828            mipmap_filter: wgpu::FilterMode::Nearest,
1829            ..Default::default()
1830        });
1831        let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1832            label: Some("roxlap-gpu scene_dda.blit_bg"),
1833            layout: &bgl_blit,
1834            entries: &[
1835                wgpu::BindGroupEntry {
1836                    binding: 0,
1837                    resource: wgpu::BindingResource::TextureView(&storage_view),
1838                },
1839                wgpu::BindGroupEntry {
1840                    binding: 1,
1841                    resource: wgpu::BindingResource::Sampler(&sampler),
1842                },
1843            ],
1844        });
1845
1846        SceneDdaResources {
1847            storage_size: (width, height),
1848            storage_view,
1849            uniform_buf,
1850            bgl_dda,
1851            pipeline_dda,
1852            blit_bg,
1853            pipeline_blit,
1854            _sampler: sampler,
1855            depth_buffer,
1856        }
1857    }
1858
1859    /// GPU.10.1 — upload a sprite model registry + its instances for
1860    /// the DDA path. An empty instance slice clears all sprites.
1861    pub fn set_sprite_instances(
1862        &mut self,
1863        registry: &sprite_model::SpriteModelRegistry,
1864        instances: &[sprite_model::SpriteInstance],
1865    ) {
1866        if instances.is_empty() {
1867            self.sprite_registry = None;
1868            return;
1869        }
1870        self.sprite_registry = Some(sprite_model::SpriteRegistryResident::upload(
1871            &self.device,
1872            registry,
1873            instances,
1874        ));
1875    }
1876
1877    /// GPU.10.4 — set the LOD pixel threshold: a sprite steps to the
1878    /// next mip once a mip-0 voxel would project below `px` screen
1879    /// pixels. `1.0` is the natural "no sub-pixel voxels" default;
1880    /// larger values force LOD in closer (useful for inspection).
1881    /// Clamped to ≥ 0.25.
1882    pub fn set_sprite_lod_px(&mut self, px: f32) {
1883        self.sprite_lod_px = px.max(0.25);
1884    }
1885
1886    /// GPU.11.1 — set the scene-grid LOD scan distance (world units).
1887    /// A chunk entered at world-t `t` is marched at mip
1888    /// `floor(log2(max(t, msd) / msd))`, clamped to its grid's mip
1889    /// ladder. `0` disables LOD (always mip-0). Larger values push
1890    /// the coarser mips farther out — the axis-aligned-mip-beams
1891    /// mitigation lever (GPU.11.2). Default 64 (matches CPU
1892    /// `mip_scan_dist`).
1893    pub fn set_scene_mip_scan_dist(&mut self, dist: f32) {
1894        self.scene_mip_scan_dist = dist.max(0.0);
1895    }
1896
1897    /// GPU.10.1 — build the instanced model-DDA pipeline (one thread
1898    /// per pixel). Lazily invoked the first frame a registry is present.
1899    fn build_sprite_model_dda(&self) -> SpriteModelDdaResources {
1900        let shader = self
1901            .device
1902            .create_shader_module(wgpu::ShaderModuleDescriptor {
1903                label: Some("sprite_model_dda.wgsl"),
1904                source: wgpu::ShaderSource::Wgsl(
1905                    include_str!("../shaders/sprite_model_dda.wgsl").into(),
1906                ),
1907            });
1908        let bgl = self
1909            .device
1910            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1911                label: Some("roxlap-gpu sprite_model_dda.bgl"),
1912                entries: &[
1913                    bgl_uniform_entry(0),
1914                    bgl_storage_entry(1, true), // occupancy
1915                    bgl_storage_entry(2, true), // colors
1916                    bgl_storage_entry(3, true), // color_offsets
1917                    bgl_storage_entry(4, true), // model_meta
1918                    bgl_storage_entry(5, true), // instances
1919                    bgl_storage_entry(6, true), // scene depth
1920                    wgpu::BindGroupLayoutEntry {
1921                        binding: 7,
1922                        visibility: wgpu::ShaderStages::COMPUTE,
1923                        ty: wgpu::BindingType::StorageTexture {
1924                            access: wgpu::StorageTextureAccess::WriteOnly,
1925                            format: wgpu::TextureFormat::Rgba8Unorm,
1926                            view_dimension: wgpu::TextureViewDimension::D2,
1927                        },
1928                        count: None,
1929                    },
1930                    bgl_storage_entry(8, true), // tile_ranges
1931                    bgl_storage_entry(9, true), // tile_instances
1932                ],
1933            });
1934        let pl = self
1935            .device
1936            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1937                label: Some("roxlap-gpu sprite_model_dda.layout"),
1938                bind_group_layouts: &[&bgl],
1939                push_constant_ranges: &[],
1940            });
1941        let pipeline = self
1942            .device
1943            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1944                label: Some("roxlap-gpu sprite_model_dda.pipeline"),
1945                layout: Some(&pl),
1946                module: &shader,
1947                entry_point: "march",
1948                compilation_options: wgpu::PipelineCompilationOptions::default(),
1949                cache: None,
1950            });
1951        let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1952            label: Some("roxlap-gpu sprite_model_dda.uniform"),
1953            size: std::mem::size_of::<SpriteModelUniform>() as u64,
1954            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1955            mapped_at_creation: false,
1956        });
1957        SpriteModelDdaResources {
1958            bgl,
1959            pipeline,
1960            uniform_buf,
1961        }
1962    }
1963}
1964
1965/// GPU.11 — headless scene-DDA renderer for tests + offline visual
1966/// gates. Owns the `scene_dda.wgsl` compute pipeline with no surface
1967/// and no blit pass; renders a [`GpuSceneResident`] to an in-memory
1968/// RGBA framebuffer via texture readback. The per-substage visual
1969/// gate (render reference scenes, diff PPMs) and the GPU.11.1 mip
1970/// render-diff both ride on this.
1971pub struct HeadlessSceneRenderer {
1972    width: u32,
1973    height: u32,
1974    output_tex: wgpu::Texture,
1975    output_view: wgpu::TextureView,
1976    depth_buffer: wgpu::Buffer,
1977    uniform_buf: wgpu::Buffer,
1978    _sky_texture: wgpu::Texture,
1979    sky_view: wgpu::TextureView,
1980    sky_sampler: wgpu::Sampler,
1981    bgl: wgpu::BindGroupLayout,
1982    pipeline: wgpu::ComputePipeline,
1983    readback: wgpu::Buffer,
1984    padded_bytes_per_row: u32,
1985}
1986
1987impl HeadlessSceneRenderer {
1988    /// Build the compute pipeline + output/readback resources for a
1989    /// `width × height` framebuffer. Validates `scene_dda.wgsl` and
1990    /// the [`scene::GridStaticMeta`] std430 layout at pipeline /
1991    /// bind-group time.
1992    #[must_use]
1993    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
1994        let output_tex = device.create_texture(&wgpu::TextureDescriptor {
1995            label: Some("roxlap-gpu headless.output"),
1996            size: wgpu::Extent3d {
1997                width,
1998                height,
1999                depth_or_array_layers: 1,
2000            },
2001            mip_level_count: 1,
2002            sample_count: 1,
2003            dimension: wgpu::TextureDimension::D2,
2004            format: wgpu::TextureFormat::Rgba8Unorm,
2005            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
2006            view_formats: &[],
2007        });
2008        let output_view = output_tex.create_view(&wgpu::TextureViewDescriptor::default());
2009
2010        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
2011            label: Some("roxlap-gpu headless.uniform"),
2012            size: std::mem::size_of::<SceneDdaUniform>() as u64,
2013            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2014            mapped_at_creation: false,
2015        });
2016        let depth_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2017            label: Some("roxlap-gpu headless.depth"),
2018            size: u64::from(width) * u64::from(height) * 4,
2019            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2020            mapped_at_creation: false,
2021        });
2022
2023        let default_sky_pixel = [120u8, 150, 220, 255];
2024        let (sky_texture, sky_view) = create_sky_texture(device, 1, 1, &default_sky_pixel);
2025        let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
2026            label: Some("roxlap-gpu headless.sky_sampler"),
2027            address_mode_u: wgpu::AddressMode::Repeat,
2028            address_mode_v: wgpu::AddressMode::Repeat,
2029            mag_filter: wgpu::FilterMode::Linear,
2030            min_filter: wgpu::FilterMode::Linear,
2031            ..Default::default()
2032        });
2033
2034        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2035            label: Some("scene_dda.wgsl (headless)"),
2036            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/scene_dda.wgsl").into()),
2037        });
2038        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2039            label: Some("roxlap-gpu headless.bgl"),
2040            entries: &[
2041                bgl_uniform_entry(0),
2042                bgl_storage_entry(1, true),
2043                bgl_storage_entry(2, true),
2044                bgl_storage_entry(3, true),
2045                bgl_storage_entry(4, true),
2046                bgl_storage_entry(5, true),
2047                bgl_storage_entry(6, true),
2048                bgl_storage_entry(7, true),
2049                wgpu::BindGroupLayoutEntry {
2050                    binding: 8,
2051                    visibility: wgpu::ShaderStages::COMPUTE,
2052                    ty: wgpu::BindingType::StorageTexture {
2053                        access: wgpu::StorageTextureAccess::WriteOnly,
2054                        format: wgpu::TextureFormat::Rgba8Unorm,
2055                        view_dimension: wgpu::TextureViewDimension::D2,
2056                    },
2057                    count: None,
2058                },
2059                wgpu::BindGroupLayoutEntry {
2060                    binding: 9,
2061                    visibility: wgpu::ShaderStages::COMPUTE,
2062                    ty: wgpu::BindingType::Texture {
2063                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
2064                        view_dimension: wgpu::TextureViewDimension::D2,
2065                        multisampled: false,
2066                    },
2067                    count: None,
2068                },
2069                wgpu::BindGroupLayoutEntry {
2070                    binding: 10,
2071                    visibility: wgpu::ShaderStages::COMPUTE,
2072                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2073                    count: None,
2074                },
2075                bgl_storage_entry(11, false),
2076                bgl_storage_entry(12, true),
2077                bgl_storage_entry(13, true),
2078                bgl_storage_entry(14, true),
2079            ],
2080        });
2081        let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2082            label: Some("roxlap-gpu headless.layout"),
2083            bind_group_layouts: &[&bgl],
2084            push_constant_ranges: &[],
2085        });
2086        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
2087            label: Some("roxlap-gpu headless.pipeline"),
2088            layout: Some(&pl),
2089            module: &shader,
2090            entry_point: "render_scene",
2091            compilation_options: wgpu::PipelineCompilationOptions::default(),
2092            cache: None,
2093        });
2094
2095        // Readback buffer: row pitch must be 256-aligned for
2096        // copy_texture_to_buffer.
2097        let padded_bytes_per_row = (width * 4).div_ceil(256) * 256;
2098        let readback = device.create_buffer(&wgpu::BufferDescriptor {
2099            label: Some("roxlap-gpu headless.readback"),
2100            size: u64::from(padded_bytes_per_row) * u64::from(height),
2101            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2102            mapped_at_creation: false,
2103        });
2104
2105        Self {
2106            width,
2107            height,
2108            output_tex,
2109            output_view,
2110            depth_buffer,
2111            uniform_buf,
2112            _sky_texture: sky_texture,
2113            sky_view,
2114            sky_sampler,
2115            bgl,
2116            pipeline,
2117            readback,
2118            padded_bytes_per_row,
2119        }
2120    }
2121
2122    /// Render `scene` from `cameras` (one per grid) and read the
2123    /// framebuffer back as `width*height` packed `0xAABBGGRR` pixels
2124    /// (R in the low byte). Fog is disabled. `mip_scan_dist` drives
2125    /// the GPU.11.1 scene-grid LOD (`0` = always mip-0). Blocks on
2126    /// readback.
2127    ///
2128    /// # Panics
2129    /// If `cameras.len() != scene.grid_count`.
2130    #[must_use]
2131    #[allow(clippy::too_many_arguments)]
2132    pub fn render(
2133        &self,
2134        device: &wgpu::Device,
2135        queue: &wgpu::Queue,
2136        scene: &GpuSceneResident,
2137        cameras: &[Camera],
2138        fov_y_rad: f32,
2139        max_outer_steps: u32,
2140        mip_scan_dist: f32,
2141    ) -> Vec<u32> {
2142        assert_eq!(
2143            cameras.len(),
2144            scene.grid_count as usize,
2145            "headless render: {} cameras for {} grids",
2146            cameras.len(),
2147            scene.grid_count,
2148        );
2149
2150        let mut cam_array = [SceneDdaPerGridCamera::zeroed(); SCENE_MAX_GRIDS];
2151        for (i, cam) in cameras.iter().enumerate() {
2152            cam_array[i] = SceneDdaPerGridCamera {
2153                pos: cam.position,
2154                _pad0: 0.0,
2155                right: cam.right,
2156                _pad1: 0.0,
2157                down: cam.down,
2158                _pad2: 0.0,
2159                forward: cam.forward,
2160                _pad3: 0.0,
2161            };
2162        }
2163        let uniform = SceneDdaUniform {
2164            fov_y_rad,
2165            grid_count: scene.grid_count,
2166            max_outer_steps,
2167            _pad0: 0,
2168            screen_size: [self.width, self.height],
2169            _pad1: [0; 2],
2170            cameras: cam_array,
2171            // Fog off: near/far past any reachable t → factor 0.
2172            fog_color: [0.0, 0.0, 0.0, 1.0e29],
2173            fog_far: 1.0e30,
2174            write_depth: 0,
2175            occ_page_words: scene.occupancy_page_words,
2176            occ_num_pages: scene.occupancy_num_pages,
2177            mip_scan_dist,
2178            _pad2: 0,
2179            _pad3: 0,
2180            _pad4: 0,
2181        };
2182        queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&uniform));
2183
2184        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2185            label: Some("roxlap-gpu headless.bg"),
2186            layout: &self.bgl,
2187            entries: &[
2188                wgpu::BindGroupEntry {
2189                    binding: 0,
2190                    resource: self.uniform_buf.as_entire_binding(),
2191                },
2192                wgpu::BindGroupEntry {
2193                    binding: 1,
2194                    resource: scene.occupancy_pages[0].as_entire_binding(),
2195                },
2196                wgpu::BindGroupEntry {
2197                    binding: 2,
2198                    resource: scene.all_color_offsets.as_entire_binding(),
2199                },
2200                wgpu::BindGroupEntry {
2201                    binding: 3,
2202                    resource: scene.all_colors.as_entire_binding(),
2203                },
2204                wgpu::BindGroupEntry {
2205                    binding: 4,
2206                    resource: scene.all_chunk_colors_base.as_entire_binding(),
2207                },
2208                wgpu::BindGroupEntry {
2209                    binding: 5,
2210                    resource: scene.all_chunk_occupancy.as_entire_binding(),
2211                },
2212                wgpu::BindGroupEntry {
2213                    binding: 6,
2214                    resource: scene.grid_static_meta.as_entire_binding(),
2215                },
2216                wgpu::BindGroupEntry {
2217                    binding: 7,
2218                    resource: scene.all_slot_chunk_idx.as_entire_binding(),
2219                },
2220                wgpu::BindGroupEntry {
2221                    binding: 8,
2222                    resource: wgpu::BindingResource::TextureView(&self.output_view),
2223                },
2224                wgpu::BindGroupEntry {
2225                    binding: 9,
2226                    resource: wgpu::BindingResource::TextureView(&self.sky_view),
2227                },
2228                wgpu::BindGroupEntry {
2229                    binding: 10,
2230                    resource: wgpu::BindingResource::Sampler(&self.sky_sampler),
2231                },
2232                wgpu::BindGroupEntry {
2233                    binding: 11,
2234                    resource: self.depth_buffer.as_entire_binding(),
2235                },
2236                wgpu::BindGroupEntry {
2237                    binding: 12,
2238                    resource: scene.occupancy_pages[1].as_entire_binding(),
2239                },
2240                wgpu::BindGroupEntry {
2241                    binding: 13,
2242                    resource: scene.occupancy_pages[2].as_entire_binding(),
2243                },
2244                wgpu::BindGroupEntry {
2245                    binding: 14,
2246                    resource: scene.occupancy_pages[3].as_entire_binding(),
2247                },
2248            ],
2249        });
2250
2251        let mut enc =
2252            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
2253        {
2254            let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
2255                label: Some("roxlap-gpu headless.pass"),
2256                timestamp_writes: None,
2257            });
2258            pass.set_pipeline(&self.pipeline);
2259            pass.set_bind_group(0, &bg, &[]);
2260            pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1);
2261        }
2262        enc.copy_texture_to_buffer(
2263            wgpu::ImageCopyTexture {
2264                texture: &self.output_tex,
2265                mip_level: 0,
2266                origin: wgpu::Origin3d::ZERO,
2267                aspect: wgpu::TextureAspect::All,
2268            },
2269            wgpu::ImageCopyBuffer {
2270                buffer: &self.readback,
2271                layout: wgpu::ImageDataLayout {
2272                    offset: 0,
2273                    bytes_per_row: Some(self.padded_bytes_per_row),
2274                    rows_per_image: Some(self.height),
2275                },
2276            },
2277            wgpu::Extent3d {
2278                width: self.width,
2279                height: self.height,
2280                depth_or_array_layers: 1,
2281            },
2282        );
2283        queue.submit(Some(enc.finish()));
2284
2285        let slice = self.readback.slice(..);
2286        let (tx, rx) = std::sync::mpsc::channel();
2287        slice.map_async(wgpu::MapMode::Read, move |r| {
2288            let _ = tx.send(r);
2289        });
2290        device.poll(wgpu::Maintain::Wait);
2291        rx.recv().expect("map_async channel").expect("map_async");
2292
2293        let data = slice.get_mapped_range();
2294        let mut out = Vec::with_capacity((self.width * self.height) as usize);
2295        let pitch = self.padded_bytes_per_row as usize;
2296        for y in 0..self.height as usize {
2297            let row = &data[y * pitch..y * pitch + self.width as usize * 4];
2298            for px in row.chunks_exact(4) {
2299                out.push(
2300                    u32::from(px[0])
2301                        | (u32::from(px[1]) << 8)
2302                        | (u32::from(px[2]) << 16)
2303                        | (u32::from(px[3]) << 24),
2304                );
2305            }
2306        }
2307        drop(data);
2308        self.readback.unmap();
2309        out
2310    }
2311}
2312
2313fn bgl_uniform_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
2314    wgpu::BindGroupLayoutEntry {
2315        binding,
2316        visibility: wgpu::ShaderStages::COMPUTE,
2317        ty: wgpu::BindingType::Buffer {
2318            ty: wgpu::BufferBindingType::Uniform,
2319            has_dynamic_offset: false,
2320            min_binding_size: None,
2321        },
2322        count: None,
2323    }
2324}
2325
2326fn bgl_storage_entry(binding: u32, read_only: bool) -> wgpu::BindGroupLayoutEntry {
2327    wgpu::BindGroupLayoutEntry {
2328        binding,
2329        visibility: wgpu::ShaderStages::COMPUTE,
2330        ty: wgpu::BindingType::Buffer {
2331            ty: wgpu::BufferBindingType::Storage { read_only },
2332            has_dynamic_offset: false,
2333            min_binding_size: None,
2334        },
2335        count: None,
2336    }
2337}
2338
2339/// Create a fresh sky panorama texture sized `width × height` with
2340/// the initial pixel data uploaded via `write_texture`. Used by
2341/// `GpuRenderer::new` (1×1 default) and `set_sky_panorama` (host-
2342/// supplied panorama).
2343fn create_sky_texture(
2344    device: &wgpu::Device,
2345    width: u32,
2346    height: u32,
2347    _initial_pixels: &[u8],
2348) -> (wgpu::Texture, wgpu::TextureView) {
2349    let tex = device.create_texture(&wgpu::TextureDescriptor {
2350        label: Some("roxlap-gpu sky_texture"),
2351        size: wgpu::Extent3d {
2352            width,
2353            height,
2354            depth_or_array_layers: 1,
2355        },
2356        mip_level_count: 1,
2357        sample_count: 1,
2358        dimension: wgpu::TextureDimension::D2,
2359        format: wgpu::TextureFormat::Rgba8Unorm,
2360        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2361        view_formats: &[],
2362    });
2363    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2364    (tex, view)
2365}
2366
2367/// GPU.4 needs to upload a whole grid (~hundreds of MiB) as a few
2368/// storage buffers. wgpu's default `max_storage_buffer_binding_size`
2369/// is 128 MiB, which is just enough for the demo's 32×32 ground
2370/// occupancy (~128 MiB) but not the colour array. We request as
2371/// much as the adapter is willing to give — most desktop GPUs cap
2372/// individual storage buffers at 2-4 GiB; iGPUs often offer the
2373/// full system memory.
2374pub(crate) fn pick_required_limits(adapter_limits: &wgpu::Limits) -> wgpu::Limits {
2375    wgpu::Limits {
2376        max_storage_buffer_binding_size: adapter_limits.max_storage_buffer_binding_size,
2377        max_buffer_size: adapter_limits.max_buffer_size,
2378        // Occupancy paging adds up to MAX_OCC_PAGES-1 extra storage
2379        // bindings; with the scene's other buffers + the GPU.9 depth
2380        // buffer the scene_dda stage needs ~11. The default cap is 8.
2381        // Both NVK and lavapipe advertise ≫16, so request 16.
2382        max_storage_buffers_per_shader_stage: adapter_limits
2383            .max_storage_buffers_per_shader_stage
2384            .min(16),
2385        ..wgpu::Limits::default()
2386    }
2387}
2388
2389fn pick_present_mode(modes: &[wgpu::PresentMode]) -> wgpu::PresentMode {
2390    // Prefer Mailbox > Immediate > Fifo. Fifo is the universal
2391    // fallback and the only one Wayland-on-Mesa always offers.
2392    for &m in &[wgpu::PresentMode::Mailbox, wgpu::PresentMode::Immediate] {
2393        if modes.contains(&m) {
2394            return m;
2395        }
2396    }
2397    wgpu::PresentMode::Fifo
2398}