Skip to main content

roxlap_gpu/
resident.rs

1//! GPU.2 — chunk-resident storage buffers + debug read-back.
2//!
3//! Uploads a [`ChunkUpload`] as three storage buffers (occupancy,
4//! per-column colour offsets, packed colour array) on a wgpu
5//! device. The [`GpuChunkResident::read_voxel_blocking`] helper
6//! dispatches the `debug_read.wgsl` shader to extract a single
7//! voxel's colour via map-async readback, validating the round trip
8//! demanded by `PORTING-GPU.md` §GPU.2.
9
10#![allow(clippy::too_many_lines, clippy::missing_panics_doc)]
11
12use std::num::NonZeroU64;
13
14use bytemuck::{Pod, Zeroable};
15use wgpu::util::DeviceExt;
16
17use crate::decompress::{ChunkUpload, CHUNK_Z};
18
19/// Uniform handed to `debug_read.wgsl` — the voxel coordinate to
20/// probe plus the chunk extents the shader needs to index occupancy.
21#[repr(C)]
22#[derive(Clone, Copy, Pod, Zeroable)]
23struct ProbeUniform {
24    coord: [u32; 3],
25    vsid: u32,
26    chunk_z: u32,
27    _pad: [u32; 3],
28}
29
30/// GPU-side storage for one decompressed chunk. Owns its buffers;
31/// dropping releases them.
32pub struct GpuChunkResident {
33    /// XY extent of the chunk in voxels (typically 128). Copied from
34    /// [`ChunkUpload::vsid`]; the probe shader needs it to index columns.
35    pub vsid: u32,
36    /// Storage buffer holding [`ChunkUpload::occupancy`]: 1 bit per
37    /// voxel, `bit(x, y, z) = (word[i >> 5] >> (i & 31)) & 1` with
38    /// `i = x + y*vsid + z*vsid*vsid`.
39    pub occupancy: wgpu::Buffer,
40    /// Storage buffer holding [`ChunkUpload::color_offsets`]:
41    /// `vsid² + 1` u32s; column `(x, y)`'s colours span
42    /// `colors[offsets[x + y*vsid] .. offsets[x + y*vsid + 1]]`.
43    pub color_offsets: wgpu::Buffer,
44    /// Storage buffer holding [`ChunkUpload::colors`]: one packed u32
45    /// per solid voxel in ascending-z column order — voxlap wire format
46    /// (blue in bits 0-7, green 8-15, red 16-23, brightness 24-31 with
47    /// `0x80` = neutral).
48    pub colors: wgpu::Buffer,
49    /// Size of [`Self::occupancy`] in bytes (word count × 4), for
50    /// memory accounting.
51    pub occupancy_bytes: u64,
52    /// Size of [`Self::color_offsets`] in bytes.
53    pub color_offsets_bytes: u64,
54    /// Size of [`Self::colors`] in bytes.
55    pub colors_bytes: u64,
56
57    // Debug-read scaffolding. In GPU.3+ the main render shader
58    // consumes the storage buffers directly; for GPU.2 these are
59    // the only consumer.
60    probe_uniform: wgpu::Buffer,
61    probe_output: wgpu::Buffer,
62    probe_readback: wgpu::Buffer,
63    probe_bg: wgpu::BindGroup,
64    probe_pipeline: wgpu::ComputePipeline,
65}
66
67impl GpuChunkResident {
68    /// Upload `chunk` to `device`. Single-shot allocation; no
69    /// streaming machinery yet — that arrives in GPU.6 / GPU.7.
70    pub fn upload(device: &wgpu::Device, chunk: &ChunkUpload) -> Self {
71        let occupancy = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
72            label: Some("roxlap-gpu chunk.occupancy"),
73            contents: bytemuck::cast_slice(&chunk.occupancy),
74            usage: wgpu::BufferUsages::STORAGE,
75        });
76        let color_offsets = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
77            label: Some("roxlap-gpu chunk.color_offsets"),
78            contents: bytemuck::cast_slice(&chunk.color_offsets),
79            usage: wgpu::BufferUsages::STORAGE,
80        });
81        let colors = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
82            label: Some("roxlap-gpu chunk.colors"),
83            contents: bytemuck::cast_slice(&chunk.colors),
84            usage: wgpu::BufferUsages::STORAGE,
85        });
86
87        let occupancy_bytes = (chunk.occupancy.len() * 4) as u64;
88        let color_offsets_bytes = (chunk.color_offsets.len() * 4) as u64;
89        let colors_bytes = (chunk.colors.len() * 4) as u64;
90
91        // Debug-read scaffolding ----------------------------------------------
92        let probe_uniform = device.create_buffer(&wgpu::BufferDescriptor {
93            label: Some("roxlap-gpu chunk.probe_uniform"),
94            size: std::mem::size_of::<ProbeUniform>() as u64,
95            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
96            mapped_at_creation: false,
97        });
98        let probe_output = device.create_buffer(&wgpu::BufferDescriptor {
99            label: Some("roxlap-gpu chunk.probe_output"),
100            size: 4,
101            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
102            mapped_at_creation: false,
103        });
104        let probe_readback = device.create_buffer(&wgpu::BufferDescriptor {
105            label: Some("roxlap-gpu chunk.probe_readback"),
106            size: 4,
107            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
108            mapped_at_creation: false,
109        });
110
111        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
112            label: Some("debug_read.wgsl"),
113            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/debug_read.wgsl").into()),
114        });
115
116        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
117            label: Some("roxlap-gpu chunk.probe_bgl"),
118            entries: &[
119                bgl_uniform_entry(0),
120                bgl_storage_entry(1, true),
121                bgl_storage_entry(2, true),
122                bgl_storage_entry(3, true),
123                bgl_storage_entry(4, false),
124            ],
125        });
126        let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
127            label: Some("roxlap-gpu chunk.probe_layout"),
128            bind_group_layouts: &[Some(&bgl)],
129            immediate_size: 0,
130        });
131        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
132            label: Some("roxlap-gpu chunk.probe_pipeline"),
133            layout: Some(&pl),
134            module: &shader,
135            entry_point: Some("debug_read"),
136            compilation_options: wgpu::PipelineCompilationOptions::default(),
137            cache: None,
138        });
139        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
140            label: Some("roxlap-gpu chunk.probe_bg"),
141            layout: &bgl,
142            entries: &[
143                wgpu::BindGroupEntry {
144                    binding: 0,
145                    resource: probe_uniform.as_entire_binding(),
146                },
147                wgpu::BindGroupEntry {
148                    binding: 1,
149                    resource: occupancy.as_entire_binding(),
150                },
151                wgpu::BindGroupEntry {
152                    binding: 2,
153                    resource: color_offsets.as_entire_binding(),
154                },
155                wgpu::BindGroupEntry {
156                    binding: 3,
157                    resource: colors.as_entire_binding(),
158                },
159                wgpu::BindGroupEntry {
160                    binding: 4,
161                    resource: probe_output.as_entire_binding(),
162                },
163            ],
164        });
165
166        Self {
167            vsid: chunk.vsid,
168            occupancy,
169            color_offsets,
170            colors,
171            occupancy_bytes,
172            color_offsets_bytes,
173            colors_bytes,
174            probe_uniform,
175            probe_output,
176            probe_readback,
177            probe_bg: bg,
178            probe_pipeline: pipeline,
179        }
180    }
181
182    /// Total resident bytes (occupancy + offsets + colours) — for
183    /// the upload-time benchmark.
184    pub fn resident_bytes(&self) -> u64 {
185        self.occupancy_bytes + self.color_offsets_bytes + self.colors_bytes
186    }
187
188    /// Round-trip read of a single voxel via the debug shader.
189    /// Returns `Some(rgb)` for a solid voxel (textured or bedrock),
190    /// `None` for empty / out-of-bounds.
191    ///
192    /// Blocks until the GPU finishes; not intended for the render
193    /// hot path. The GPU.2 validation test is the only caller (native).
194    pub fn read_voxel_blocking(
195        &self,
196        device: &wgpu::Device,
197        queue: &wgpu::Queue,
198        x: u32,
199        y: u32,
200        z: u32,
201    ) -> Option<u32> {
202        let uniform = ProbeUniform {
203            coord: [x, y, z],
204            vsid: self.vsid,
205            chunk_z: CHUNK_Z,
206            _pad: [0; 3],
207        };
208        queue.write_buffer(&self.probe_uniform, 0, bytemuck::bytes_of(&uniform));
209
210        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
211            label: Some("roxlap-gpu chunk.read_voxel"),
212        });
213        {
214            let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
215                label: Some("roxlap-gpu chunk.debug_read"),
216                timestamp_writes: None,
217            });
218            cpass.set_pipeline(&self.probe_pipeline);
219            cpass.set_bind_group(0, &self.probe_bg, &[]);
220            cpass.dispatch_workgroups(1, 1, 1);
221        }
222        encoder.copy_buffer_to_buffer(&self.probe_output, 0, &self.probe_readback, 0, 4);
223        queue.submit(std::iter::once(encoder.finish()));
224
225        // Map the readback buffer. wgpu's map_async runs the
226        // callback when the device.poll(Wait) services it; pollster
227        // turns the resulting future into a blocking wait.
228        let slice = self.probe_readback.slice(..);
229        let (tx, rx) = std::sync::mpsc::channel();
230        slice.map_async(wgpu::MapMode::Read, move |res| {
231            tx.send(res).expect("send map result");
232        });
233        device.poll(wgpu::PollType::wait_indefinitely()).ok();
234        rx.recv()
235            .expect("recv map result")
236            .expect("map_async returned an error");
237
238        let bytes = slice.get_mapped_range();
239        let value = u32::from_le_bytes(bytes[..4].try_into().expect("4 bytes"));
240        drop(bytes);
241        self.probe_readback.unmap();
242
243        if value == 0 {
244            None
245        } else {
246            Some(value)
247        }
248    }
249}
250
251fn bgl_uniform_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
252    wgpu::BindGroupLayoutEntry {
253        binding,
254        visibility: wgpu::ShaderStages::COMPUTE,
255        ty: wgpu::BindingType::Buffer {
256            ty: wgpu::BufferBindingType::Uniform,
257            has_dynamic_offset: false,
258            min_binding_size: NonZeroU64::new(std::mem::size_of::<ProbeUniform>() as u64),
259        },
260        count: None,
261    }
262}
263
264fn bgl_storage_entry(binding: u32, read_only: bool) -> wgpu::BindGroupLayoutEntry {
265    wgpu::BindGroupLayoutEntry {
266        binding,
267        visibility: wgpu::ShaderStages::COMPUTE,
268        ty: wgpu::BindingType::Buffer {
269            ty: wgpu::BufferBindingType::Storage { read_only },
270            has_dynamic_offset: false,
271            min_binding_size: None,
272        },
273        count: None,
274    }
275}