1use crate::core::renderer::Vertex;
2use crate::core::scene::GpuVertexBuffer;
3use crate::gpu::axis::{axis_storage_buffer, AxisData};
4use crate::gpu::shaders;
5use crate::gpu::{tuning, ScalarType};
6use std::sync::Arc;
7use wgpu::util::DeviceExt;
8
9pub type SurfaceAxis<'a> = AxisData<'a>;
11
12pub struct SurfaceGpuInputs<'a> {
14 pub x_axis: SurfaceAxis<'a>,
15 pub y_axis: SurfaceAxis<'a>,
16 pub z_buffer: Arc<wgpu::Buffer>,
17 pub color_table: &'a [[f32; 4]],
18 pub x_len: u32,
19 pub y_len: u32,
20 pub scalar: ScalarType,
21}
22
23pub struct SurfaceGpuParams {
25 pub min_z: f32,
26 pub max_z: f32,
27 pub alpha: f32,
28 pub flatten_z: bool,
29 pub x_stride: u32,
30 pub y_stride: u32,
31 pub lod_x_len: u32,
32 pub lod_y_len: u32,
33}
34
35#[repr(C)]
36#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
37struct SurfaceUniforms {
38 min_z: f32,
39 max_z: f32,
40 alpha: f32,
41 flatten: u32,
42 x_len: u32,
43 y_len: u32,
44 lod_x_len: u32,
45 lod_y_len: u32,
46 x_stride: u32,
47 y_stride: u32,
48 color_table_len: u32,
49 _pad: u32,
50}
51
52pub fn pack_surface_vertices(
54 device: &Arc<wgpu::Device>,
55 queue: &Arc<wgpu::Queue>,
56 inputs: &SurfaceGpuInputs<'_>,
57 params: &SurfaceGpuParams,
58) -> Result<GpuVertexBuffer, String> {
59 if inputs.x_len < 2 || inputs.y_len < 2 {
60 return Err("surf: axis vectors must contain at least two elements".to_string());
61 }
62
63 let workgroup_size = tuning::effective_workgroup_size();
64 let shader = compile_shader(device, workgroup_size, inputs.scalar);
65
66 let x_buffer = axis_storage_buffer(device, "surface-x-axis", &inputs.x_axis, inputs.scalar)?;
67 let y_buffer = axis_storage_buffer(device, "surface-y-axis", &inputs.y_axis, inputs.scalar)?;
68
69 let color_buffer = Arc::new(
70 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
71 label: Some("surface-color-table"),
72 contents: bytemuck::cast_slice(inputs.color_table),
73 usage: wgpu::BufferUsages::STORAGE
74 | wgpu::BufferUsages::COPY_DST
75 | wgpu::BufferUsages::COPY_SRC,
76 }),
77 );
78
79 let lod_x_len = params.lod_x_len.max(1);
80 let lod_y_len = params.lod_y_len.max(1);
81 let vertex_count = lod_x_len
82 .checked_mul(lod_y_len)
83 .ok_or_else(|| "surf: grid dimensions overflowed vertex count".to_string())?;
84 let output_size = vertex_count as u64 * std::mem::size_of::<Vertex>() as u64;
85 let output_buffer = Arc::new(device.create_buffer(&wgpu::BufferDescriptor {
86 label: Some("surface-gpu-vertices"),
87 size: output_size,
88 usage: wgpu::BufferUsages::STORAGE
89 | wgpu::BufferUsages::VERTEX
90 | wgpu::BufferUsages::COPY_DST
91 | wgpu::BufferUsages::COPY_SRC,
92 mapped_at_creation: false,
93 }));
94
95 let min_z = if params.min_z.is_finite() {
96 params.min_z
97 } else {
98 tracing::warn!(
99 target: "runmat_plot::surface_gpu",
100 min_z = params.min_z,
101 "non-finite min_z received; sanitizing to 0.0"
102 );
103 0.0
104 };
105 let mut max_z = if params.max_z.is_finite() {
106 params.max_z
107 } else {
108 tracing::warn!(
109 target: "runmat_plot::surface_gpu",
110 max_z = params.max_z,
111 "non-finite max_z received; sanitizing to min_z + 1.0"
112 );
113 min_z + 1.0
114 };
115 if max_z <= min_z {
116 tracing::warn!(
117 target: "runmat_plot::surface_gpu",
118 min_z,
119 max_z,
120 "invalid z range received; forcing epsilon span"
121 );
122 max_z = min_z + 1e-6;
123 }
124
125 let uniforms = SurfaceUniforms {
126 min_z,
127 max_z,
128 alpha: params.alpha,
129 flatten: if params.flatten_z { 1 } else { 0 },
130 x_len: inputs.x_len,
131 y_len: inputs.y_len,
132 lod_x_len,
133 lod_y_len,
134 x_stride: params.x_stride.max(1),
135 y_stride: params.y_stride.max(1),
136 color_table_len: inputs.color_table.len() as u32,
137 _pad: 0,
138 };
139 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
140 label: Some("surface-pack-uniforms"),
141 contents: bytemuck::bytes_of(&uniforms),
142 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
143 });
144
145 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
146 label: Some("surface-pack-bind-layout"),
147 entries: &[
148 wgpu::BindGroupLayoutEntry {
149 binding: 0,
150 visibility: wgpu::ShaderStages::COMPUTE,
151 ty: wgpu::BindingType::Buffer {
152 ty: wgpu::BufferBindingType::Storage { read_only: true },
153 has_dynamic_offset: false,
154 min_binding_size: None,
155 },
156 count: None,
157 },
158 wgpu::BindGroupLayoutEntry {
159 binding: 1,
160 visibility: wgpu::ShaderStages::COMPUTE,
161 ty: wgpu::BindingType::Buffer {
162 ty: wgpu::BufferBindingType::Storage { read_only: true },
163 has_dynamic_offset: false,
164 min_binding_size: None,
165 },
166 count: None,
167 },
168 wgpu::BindGroupLayoutEntry {
169 binding: 2,
170 visibility: wgpu::ShaderStages::COMPUTE,
171 ty: wgpu::BindingType::Buffer {
172 ty: wgpu::BufferBindingType::Storage { read_only: true },
173 has_dynamic_offset: false,
174 min_binding_size: None,
175 },
176 count: None,
177 },
178 wgpu::BindGroupLayoutEntry {
179 binding: 3,
180 visibility: wgpu::ShaderStages::COMPUTE,
181 ty: wgpu::BindingType::Buffer {
182 ty: wgpu::BufferBindingType::Storage { read_only: true },
183 has_dynamic_offset: false,
184 min_binding_size: None,
185 },
186 count: None,
187 },
188 wgpu::BindGroupLayoutEntry {
189 binding: 4,
190 visibility: wgpu::ShaderStages::COMPUTE,
191 ty: wgpu::BindingType::Buffer {
192 ty: wgpu::BufferBindingType::Storage { read_only: false },
193 has_dynamic_offset: false,
194 min_binding_size: None,
195 },
196 count: None,
197 },
198 wgpu::BindGroupLayoutEntry {
199 binding: 5,
200 visibility: wgpu::ShaderStages::COMPUTE,
201 ty: wgpu::BindingType::Buffer {
202 ty: wgpu::BufferBindingType::Uniform,
203 has_dynamic_offset: false,
204 min_binding_size: None,
205 },
206 count: None,
207 },
208 ],
209 });
210
211 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
212 label: Some("surface-pack-pipeline-layout"),
213 bind_group_layouts: &[&bind_group_layout],
214 push_constant_ranges: &[],
215 });
216
217 let pipeline =
218 device.create_compute_pipeline(&crate::wgpu_compat::wgpu_compute_pipeline_descriptor! {
219 label: Some("surface-pack-pipeline"),
220 layout: Some(&pipeline_layout),
221 module: &shader,
222 entry_point: "main",
223 });
224
225 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
226 label: Some("surface-pack-bind-group"),
227 layout: &bind_group_layout,
228 entries: &[
229 wgpu::BindGroupEntry {
230 binding: 0,
231 resource: x_buffer.as_ref().as_entire_binding(),
232 },
233 wgpu::BindGroupEntry {
234 binding: 1,
235 resource: y_buffer.as_ref().as_entire_binding(),
236 },
237 wgpu::BindGroupEntry {
238 binding: 2,
239 resource: inputs.z_buffer.as_ref().as_entire_binding(),
240 },
241 wgpu::BindGroupEntry {
242 binding: 3,
243 resource: color_buffer.as_ref().as_entire_binding(),
244 },
245 wgpu::BindGroupEntry {
246 binding: 4,
247 resource: output_buffer.as_ref().as_entire_binding(),
248 },
249 wgpu::BindGroupEntry {
250 binding: 5,
251 resource: uniform_buffer.as_entire_binding(),
252 },
253 ],
254 });
255
256 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
257 label: Some("surface-pack-encoder"),
258 });
259 {
260 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
261 label: Some("surface-pack-pass"),
262 timestamp_writes: None,
263 });
264 pass.set_pipeline(&pipeline);
265 pass.set_bind_group(0, &bind_group, &[]);
266 let workgroups = vertex_count.div_ceil(workgroup_size);
267 pass.dispatch_workgroups(workgroups, 1, 1);
268 }
269 queue.submit(Some(encoder.finish()));
270
271 Ok(GpuVertexBuffer::new(output_buffer, vertex_count as usize))
272}
273
274fn compile_shader(
275 device: &Arc<wgpu::Device>,
276 workgroup_size: u32,
277 scalar: ScalarType,
278) -> wgpu::ShaderModule {
279 let template = match scalar {
280 ScalarType::F32 => shaders::surface::F32,
281 ScalarType::F64 => shaders::surface::F64,
282 };
283 let source = template.replace("{{WORKGROUP_SIZE}}", &workgroup_size.to_string());
284 device.create_shader_module(wgpu::ShaderModuleDescriptor {
285 label: Some("surface-pack-shader"),
286 source: wgpu::ShaderSource::Wgsl(source.into()),
287 })
288}
289
290#[cfg(test)]
291mod stress_tests {
292 use super::*;
293 use pollster::FutureExt;
294
295 fn maybe_device() -> Option<(Arc<wgpu::Device>, Arc<wgpu::Queue>)> {
296 if std::env::var("RUNMAT_PLOT_SKIP_GPU_TESTS").is_ok()
297 || std::env::var("RUNMAT_PLOT_FORCE_GPU_TESTS").is_err()
298 {
299 return None;
300 }
301 let instance = wgpu::Instance::default();
302 let adapter = instance
303 .request_adapter(&wgpu::RequestAdapterOptions {
304 power_preference: wgpu::PowerPreference::HighPerformance,
305 compatible_surface: None,
306 force_fallback_adapter: false,
307 })
308 .block_on()?;
309 let limits = adapter.limits();
310 let (device, queue) = adapter
311 .request_device(
312 &crate::wgpu_compat::device_descriptor(
313 Some("runmat-plot-surface-test-device"),
314 wgpu::Features::empty(),
315 limits,
316 ),
317 None,
318 )
319 .block_on()
320 .ok()?;
321 Some((Arc::new(device), Arc::new(queue)))
322 }
323
324 #[test]
325 fn gpu_packer_handles_large_surface() {
326 let Some((device, queue)) = maybe_device() else {
327 return;
328 };
329 let x_len = 2048u32;
330 let y_len = 2048u32;
331 let total = (x_len * y_len) as usize;
332 let x_axis: Vec<f32> = (0..x_len).map(|i| i as f32 * 0.1).collect();
333 let y_axis: Vec<f32> = (0..y_len).map(|i| i as f32 * 0.1).collect();
334 let mut z_data = vec![0.0f32; total];
335 for (idx, value) in z_data.iter_mut().enumerate() {
336 let x = (idx % x_len as usize) as f32 * 0.01;
337 let y = (idx / x_len as usize) as f32 * 0.01;
338 *value = (x.sin() + y.cos()) * 0.5;
339 }
340 let z_buffer = Arc::new(
341 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
342 label: Some("surface-test-z"),
343 contents: bytemuck::cast_slice(&z_data),
344 usage: wgpu::BufferUsages::STORAGE,
345 }),
346 );
347
348 let color_table: Vec<[f32; 4]> = (0..256)
349 .map(|i| {
350 let t = i as f32 / 255.0;
351 [t, 1.0 - t, 0.5, 1.0]
352 })
353 .collect();
354
355 let inputs = SurfaceGpuInputs {
356 x_axis: SurfaceAxis::F32(&x_axis),
357 y_axis: SurfaceAxis::F32(&y_axis),
358 z_buffer,
359 color_table: &color_table,
360 x_len,
361 y_len,
362 scalar: ScalarType::F32,
363 };
364 let stride = 8;
365 let lod_x_len = x_len.div_ceil(stride);
366 let lod_y_len = y_len.div_ceil(stride);
367 let params = SurfaceGpuParams {
368 min_z: -1.0,
369 max_z: 1.0,
370 alpha: 1.0,
371 flatten_z: false,
372 x_stride: stride,
373 y_stride: stride,
374 lod_x_len,
375 lod_y_len,
376 };
377
378 let gpu_vertices =
379 pack_surface_vertices(&device, &queue, &inputs, ¶ms).expect("surface pack failed");
380 assert!(gpu_vertices.vertex_count > 0);
381 assert_eq!(gpu_vertices.vertex_count, (lod_x_len * lod_y_len) as usize);
382 }
383}