1use anyhow::{Context, Result};
2use bytemuck::{Pod, Zeroable};
3use loam_math::Rotor4;
4use wgpu::*;
5
6use crate::pass::ColorLoad;
7
8const INITIAL_BODY_CAPACITY: usize = 32;
9
10pub const SHAPE_PENTATOPE: u32 = 0;
12pub const SHAPE_TESSERACT: u32 = 1;
13pub const SHAPE_16CELL: u32 = 2;
14pub const SHAPE_24CELL: u32 = 3;
15pub const SHAPE_120CELL: u32 = 4;
16pub const SHAPE_600CELL: u32 = 5;
17pub const SHAPE_3SPHERE: u32 = 6;
18pub const SHAPE_DUOCYLINDER: u32 = 7;
19pub const SHAPE_CLIFFORD_TORUS: u32 = 8;
20pub const SHAPE_SPHERINDER: u32 = 9;
21
22#[repr(C)]
24#[derive(Copy, Clone, Debug, Pod, Zeroable)]
25pub struct BodyUniform {
26 pub position: [f32; 4],
27 pub kind: f32,
28 pub radius_or_shape: f32,
29 pub polytope_size: f32,
30 pub _pad0: f32,
31 pub color: [f32; 3],
32 pub _pad1: f32,
33 pub rotor: [f32; 8],
34}
35
36impl Default for BodyUniform {
37 fn default() -> Self {
38 Self {
39 position: [0.0; 4],
40 kind: BodyKind::Invalid as i32 as f32,
41 radius_or_shape: 0.0,
42 polytope_size: 0.0,
43 _pad0: 0.0,
44 color: [0.7, 0.7, 0.7],
45 _pad1: 0.0,
46 rotor: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
47 }
48 }
49}
50
51#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub enum BodyKind {
54 Sphere = 0,
55 Polytope = 1,
56 Invalid = 255,
57}
58
59impl BodyUniform {
60 pub fn sphere(position: [f32; 4], radius: f32, color: [f32; 3]) -> Self {
61 Self {
62 position,
63 kind: BodyKind::Sphere as i32 as f32,
64 radius_or_shape: radius,
65 color,
66 ..Self::default()
67 }
68 }
69
70 pub fn polytope(
71 position: [f32; 4],
72 shape_index: u32,
73 size: f32,
74 rotor: [f32; 8],
75 color: [f32; 3],
76 ) -> Self {
77 Self {
78 position,
79 kind: BodyKind::Polytope as i32 as f32,
80 radius_or_shape: shape_index as f32,
81 polytope_size: size,
82 color,
83 rotor,
84 ..Self::default()
85 }
86 }
87
88 pub fn polytope_with_rotor(
89 position: [f32; 4],
90 shape_index: u32,
91 size: f32,
92 rotor: Rotor4,
93 color: [f32; 3],
94 ) -> Self {
95 Self::polytope(position, shape_index, size, rotor.into(), color)
96 }
97}
98
99#[repr(C)]
101#[derive(Copy, Clone, Debug, Pod, Zeroable)]
102pub struct Hyperslice4DUniforms {
103 pub camera_pos: [f32; 3],
104 pub _pad0: f32,
105 pub camera_forward: [f32; 3],
106 pub _pad1: f32,
107 pub camera_right: [f32; 3],
108 pub _pad2: f32,
109 pub camera_up: [f32; 3],
110 pub fov_y_tan: f32,
111 pub resolution: [f32; 2],
112 pub time: f32,
113 pub tick: f32,
114 pub w_slice: f32,
115 pub body_count: f32,
116 pub viewport_origin: [f32; 2],
118 pub params: [f32; 4],
119 pub near: f32,
120 pub body_offset: f32,
121 pub _pad3: [f32; 2],
122 pub sky_below: [f32; 3],
123 pub _pad4: f32,
124 pub sky_above: [f32; 3],
125 pub _pad5: f32,
126}
127
128impl Default for Hyperslice4DUniforms {
129 fn default() -> Self {
130 Self {
131 camera_pos: [0.0, 0.0, 5.0],
132 _pad0: 0.0,
133 camera_forward: [0.0, 0.0, -1.0],
134 _pad1: 0.0,
135 camera_right: [1.0, 0.0, 0.0],
136 _pad2: 0.0,
137 camera_up: [0.0, 1.0, 0.0],
138 fov_y_tan: (60.0_f32.to_radians() * 0.5).tan(),
139 resolution: [1.0, 1.0],
140 time: 0.0,
141 tick: 0.0,
142 w_slice: 0.0,
143 body_count: 0.0,
144 viewport_origin: [0.0, 0.0],
145 params: [0.0; 4],
146 near: 0.05,
147 body_offset: 0.0,
148 _pad3: [0.0; 2],
149 sky_below: crate::sky_ground::DEFAULT_SKY.below,
150 _pad4: 0.0,
151 sky_above: crate::sky_ground::DEFAULT_SKY.above,
152 _pad5: 0.0,
153 }
154 }
155}
156
157pub const HYPERSLICE_KERNEL_WGSL: &str = concat!(
159 include_str!("../sky_ground.wgsl"),
160 include_str!("../shader/projective_depth.wgsl"),
161 r#"
162const NO_BODY: u32 = 0xffffffffu;
163
164const BODY_KIND_SPHERE: u32 = 0u;
165const BODY_KIND_POLYTOPE: u32 = 1u;
166
167const SHAPE_PENTATOPE: u32 = 0u;
168const SHAPE_TESSERACT: u32 = 1u;
169const SHAPE_16CELL: u32 = 2u;
170const SHAPE_24CELL: u32 = 3u;
171const SHAPE_120CELL: u32 = 4u;
172const SHAPE_600CELL: u32 = 5u;
173const SHAPE_3SPHERE: u32 = 6u;
174const SHAPE_DUOCYLINDER: u32 = 7u;
175const SHAPE_CLIFFORD_TORUS: u32 = 8u;
176const SHAPE_SPHERINDER: u32 = 9u;
177const BODY_KIND_INVALID: u32 = 255u;
178
179struct BodyUniform {
180 position: vec4<f32>,
181 kind: f32,
182 radius_or_shape: f32,
183 polytope_size: f32,
184 _pad0: f32,
185 color: vec3<f32>,
186 _pad1: f32,
187 // Two vec4 so the std140 stride matches Rust's `[f32; 8]`; order [s, xy, xz, xw, yz, yw, zw, xyzw].
188 rotor_lo: vec4<f32>,
189 rotor_hi: vec4<f32>,
190};
191
192struct Uniforms {
193 camera_pos: vec3<f32>,
194 camera_forward: vec3<f32>,
195 camera_right: vec3<f32>,
196 camera_up: vec3<f32>,
197 fov_y_tan: f32,
198 resolution: vec2<f32>,
199 time: f32,
200 tick: f32,
201 w_slice: f32,
202 body_count: f32,
203 viewport_origin: vec2<f32>,
204 params: vec4<f32>,
205 near: f32,
206 body_offset: f32,
207 sky_below: vec3<f32>,
208 sky_above: vec3<f32>,
209};
210
211@group(0) @binding(0) var<uniform> u: Uniforms;
212@group(0) @binding(1) var<storage, read> bodies: array<BodyUniform>;
213
214fn body_at(index: u32) -> BodyUniform {
215 return bodies[u32(u.body_offset + 0.5) + index];
216}
217
218fn body_sphere_sdf_4d(p4: vec4<f32>, b: BodyUniform) -> f32 {
219 return length(p4 - b.position) - b.radius_or_shape;
220}
221
222fn rotor4_inverse_apply(rotor_lo: vec4<f32>, rotor_hi: vec4<f32>, v: vec4<f32>) -> vec4<f32> {
223 let rs = rotor_lo.x;
224 let rxy = -rotor_lo.y;
225 let rxz = -rotor_lo.z;
226 let rxw = -rotor_lo.w;
227 let ryz = -rotor_hi.x;
228 let ryw = -rotor_hi.y;
229 let rzw = -rotor_hi.z;
230 let r_i = rotor_hi.w;
231
232 let vx = v.x; let vy = v.y; let vz = v.z; let vw = v.w;
233
234 let p1 = rs * vx - rxy * vy - rxz * vz - rxw * vw;
235 let p2 = rs * vy + rxy * vx - ryz * vz - ryw * vw;
236 let p3 = rs * vz + rxz * vx + ryz * vy - rzw * vw;
237 let p4 = rs * vw + rxw * vx + ryw * vy + rzw * vz;
238
239 let t123 = -rxy * vz + rxz * vy - ryz * vx + r_i * vw;
240 let t124 = -rxy * vw + rxw * vy - ryw * vx - r_i * vz;
241 let t134 = -rxz * vw + rxw * vz - rzw * vx + r_i * vy;
242 let t234 = -ryz * vw + ryw * vz - rzw * vy - r_i * vx;
243
244 let q1 = rs * p1 - rxy * p2 - rxz * p3 - rxw * p4
245 - ryz * t123 - ryw * t124 - rzw * t134 + r_i * t234;
246 let q2 = rs * p2 + rxy * p1 - ryz * p3 - ryw * p4
247 + rxz * t123 + rxw * t124 - rzw * t234 - r_i * t134;
248 let q3 = rs * p3 + rxz * p1 + ryz * p2 - rzw * p4
249 - rxy * t123 + rxw * t134 + ryw * t234 + r_i * t124;
250 let q4 = rs * p4 + rxw * p1 + ryw * p2 + rzw * p3
251 - rxy * t124 - rxz * t134 - ryz * t234 - r_i * t123;
252
253 return vec4<f32>(q1, q2, q3, q4);
254}
255
256fn pentatope_sdf_local(p: vec4<f32>) -> f32 {
257 let t = 0.55901699437; // sqrt(5) / 4
258 let n0 = vec4<f32>(0.0, 0.0, 0.0, -1.0);
259 let n1 = vec4<f32>(-t, -t, -t, 0.25);
260 let n2 = vec4<f32>(-t, t, t, 0.25);
261 let n3 = vec4<f32>( t, -t, t, 0.25);
262 let n4 = vec4<f32>( t, t, -t, 0.25);
263 let r = 0.25;
264 var d: f32 = dot(n0, p) - r;
265 d = max(d, dot(n1, p) - r);
266 d = max(d, dot(n2, p) - r);
267 d = max(d, dot(n3, p) - r);
268 d = max(d, dot(n4, p) - r);
269 return d;
270}
271
272fn tesseract_sdf_local(p: vec4<f32>) -> f32 {
273 let q = abs(p) - vec4<f32>(0.5, 0.5, 0.5, 0.5);
274 let outside = length(max(q, vec4<f32>(0.0, 0.0, 0.0, 0.0)));
275 let inside = min(max(max(q.x, q.y), max(q.z, q.w)), 0.0);
276 return outside + inside;
277}
278
279fn cell16_sdf_local(p: vec4<f32>) -> f32 {
280 let q = abs(p);
281 return (q.x + q.y + q.z + q.w - 1.0) * 0.5;
282}
283
284fn cell24_sdf_local(p: vec4<f32>) -> f32 {
285 let inv_sqrt2: f32 = 0.70710678;
286 let sqrt2: f32 = 1.41421356;
287 let q = abs(p);
288 let tess = max(max(q.x, q.y), max(q.z, q.w)) - inv_sqrt2;
289 let cross = (q.x + q.y + q.z + q.w - sqrt2) * 0.5;
290 return max(tess, cross);
291}
292
293fn sphere3_sdf_local(p: vec4<f32>) -> f32 {
294 return length(p) - 1.0;
295}
296
297fn duocylinder_sdf_local(p: vec4<f32>) -> f32 {
298 let r = 0.7071068;
299 let dxy = length(p.xy) - r;
300 let dzw = length(p.zw) - r;
301 let outside = length(vec2<f32>(max(dxy, 0.0), max(dzw, 0.0)));
302 let inside = min(max(dxy, dzw), 0.0);
303 return outside + inside;
304}
305
306fn clifford_torus_sdf_local(p: vec4<f32>) -> f32 {
307 let r1 = 0.5;
308 let r2 = 0.5;
309 let tube = 0.2;
310 let q1 = length(p.xy) - r1;
311 let q2 = length(p.zw) - r2;
312 return length(vec2<f32>(q1, q2)) - tube;
313}
314
315fn spherinder_sdf_local(p: vec4<f32>) -> f32 {
316 let r = 0.7071068;
317 let h = 0.7071068;
318 let dxyz = length(p.xyz) - r;
319 let dw = abs(p.w) - h;
320 let outside = length(vec2<f32>(max(dxyz, 0.0), max(dw, 0.0)));
321 let inside = min(max(dxyz, dw), 0.0);
322 return outside + inside;
323}
324
325// Outside 1.5× the circumradius the ball SDF is a Lipschitz-1 lower bound; skips the rotor inverse.
326fn body_polytope_sdf_4d(p4: vec4<f32>, b: BodyUniform) -> f32 {
327 let size = max(b.polytope_size, 1.0e-6);
328 let world_v = p4 - b.position;
329 let world_dist2 = dot(world_v, world_v);
330 let bound = size * 1.5;
331 if (world_dist2 > bound * bound) {
332 return sqrt(world_dist2) - size;
333 }
334 let local_v = rotor4_inverse_apply(b.rotor_lo, b.rotor_hi, world_v);
335 let unit_p = local_v / size;
336 let shape = u32(b.radius_or_shape + 0.5);
337 var d: f32 = 1.0e9;
338 if (shape == SHAPE_PENTATOPE) {
339 d = pentatope_sdf_local(unit_p);
340 } else if (shape == SHAPE_TESSERACT) {
341 d = tesseract_sdf_local(unit_p);
342 } else if (shape == SHAPE_16CELL) {
343 d = cell16_sdf_local(unit_p);
344 } else if (shape == SHAPE_24CELL) {
345 d = cell24_sdf_local(unit_p);
346 } else if (shape == SHAPE_120CELL) {
347 d = cell120_sdf_local(unit_p);
348 } else if (shape == SHAPE_600CELL) {
349 d = cell600_sdf_local(unit_p);
350 } else if (shape == SHAPE_3SPHERE) {
351 d = sphere3_sdf_local(unit_p);
352 } else if (shape == SHAPE_DUOCYLINDER) {
353 d = duocylinder_sdf_local(unit_p);
354 } else if (shape == SHAPE_CLIFFORD_TORUS) {
355 d = clifford_torus_sdf_local(unit_p);
356 } else if (shape == SHAPE_SPHERINDER) {
357 d = spherinder_sdf_local(unit_p);
358 }
359 return d * size;
360}
361
362fn loam_dynamic_bodies_sdf(p3: vec3<f32>) -> f32 {
363 let p4 = vec4<f32>(p3, u.w_slice);
364 let body_count = u32(u.body_count + 0.5);
365 var sdf: f32 = 1.0e9;
366 for (var i: u32 = 0u; i < body_count; i = i + 1u) {
367 let b = body_at(i);
368 let kind = u32(b.kind + 0.5);
369 if (kind == BODY_KIND_SPHERE) {
370 sdf = min(sdf, body_sphere_sdf_4d(p4, b));
371 } else if (kind == BODY_KIND_POLYTOPE) {
372 sdf = min(sdf, body_polytope_sdf_4d(p4, b));
373 }
374 }
375 return sdf;
376}
377
378fn loam_body_sdf_at(p3: vec3<f32>, body_idx: u32) -> f32 {
379 if (body_idx >= u32(u.body_count + 0.5)) { return 1.0e9; }
380 let p4 = vec4<f32>(p3, u.w_slice);
381 let b = body_at(body_idx);
382 let kind = u32(b.kind + 0.5);
383 if (kind == BODY_KIND_SPHERE) {
384 return body_sphere_sdf_4d(p4, b);
385 } else if (kind == BODY_KIND_POLYTOPE) {
386 return body_polytope_sdf_4d(p4, b);
387 }
388 return 1.0e9;
389}
390
391struct HitInfo {
392 dist: f32,
393 body_idx: u32,
394};
395
396fn loam_total_sdf(p3: vec3<f32>) -> HitInfo {
397 let scene_d = loam_scene_sdf(p3);
398
399 let p4 = vec4<f32>(p3, u.w_slice);
400 let body_count = u32(u.body_count + 0.5);
401 var dyn_d: f32 = 1.0e9;
402 var dyn_idx: u32 = NO_BODY;
403 for (var i: u32 = 0u; i < body_count; i = i + 1u) {
404 let b = body_at(i);
405 let kind = u32(b.kind + 0.5);
406 var d: f32 = 1.0e9;
407 if (kind == BODY_KIND_SPHERE) {
408 d = body_sphere_sdf_4d(p4, b);
409 } else if (kind == BODY_KIND_POLYTOPE) {
410 d = body_polytope_sdf_4d(p4, b);
411 }
412 if (d < dyn_d) {
413 dyn_d = d;
414 dyn_idx = i;
415 }
416 }
417
418 if (scene_d <= dyn_d) {
419 return HitInfo(scene_d, NO_BODY);
420 }
421 return HitInfo(dyn_d, dyn_idx);
422}
423
424@vertex
425fn vs_fullscreen(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
426 let uv = vec2<f32>(f32((vid << 1u) & 2u), f32(vid & 2u));
427 return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
428}
429
430fn estimate_normal(p: vec3<f32>, body_idx: u32) -> vec3<f32> {
431 let h = 0.001;
432 if (body_idx == NO_BODY) {
433 let dx = loam_scene_sdf(p + vec3<f32>(h, 0.0, 0.0))
434 - loam_scene_sdf(p - vec3<f32>(h, 0.0, 0.0));
435 let dy = loam_scene_sdf(p + vec3<f32>(0.0, h, 0.0))
436 - loam_scene_sdf(p - vec3<f32>(0.0, h, 0.0));
437 let dz = loam_scene_sdf(p + vec3<f32>(0.0, 0.0, h))
438 - loam_scene_sdf(p - vec3<f32>(0.0, 0.0, h));
439 return normalize(vec3<f32>(dx, dy, dz));
440 }
441 let dx = loam_body_sdf_at(p + vec3<f32>(h, 0.0, 0.0), body_idx)
442 - loam_body_sdf_at(p - vec3<f32>(h, 0.0, 0.0), body_idx);
443 let dy = loam_body_sdf_at(p + vec3<f32>(0.0, h, 0.0), body_idx)
444 - loam_body_sdf_at(p - vec3<f32>(0.0, h, 0.0), body_idx);
445 let dz = loam_body_sdf_at(p + vec3<f32>(0.0, 0.0, h), body_idx)
446 - loam_body_sdf_at(p - vec3<f32>(0.0, 0.0, h), body_idx);
447 return normalize(vec3<f32>(dx, dy, dz));
448}
449
450struct Shaded {
451 color: vec4<f32>,
452 depth: f32,
453};
454
455struct Fragment {
456 @location(0) color: vec4<f32>,
457 @builtin(frag_depth) depth: f32,
458};
459
460fn shade(frag_pos: vec4<f32>) -> Shaded {
461 let uv = ((frag_pos.xy - u.viewport_origin) / u.resolution) * 2.0 - vec2<f32>(1.0, 1.0);
462 let aspect = u.resolution.x / u.resolution.y;
463 let ndc = vec2<f32>(uv.x * aspect, -uv.y);
464 let rd = normalize(
465 u.camera_forward
466 + u.camera_right * (ndc.x * u.fov_y_tan)
467 + u.camera_up * (ndc.y * u.fov_y_tan)
468 );
469 let ro = u.camera_pos;
470
471 var t: f32 = 0.0;
472 let scene_max_t = loam_scene_max_t(ro, rd);
473 let max_t = min(60.0, scene_max_t + 1.0);
474 var hit = false;
475 var hit_idx: u32 = NO_BODY;
476 // Every composed SDF is a 1-Lipschitz lower bound, so the full step cannot tunnel.
477 let hit_eps = 0.001;
478 let min_step = 0.0001;
479 for (var i: i32 = 0; i < 384; i = i + 1) {
480 let p = ro + rd * t;
481 let h = loam_total_sdf(p);
482 if (h.dist < hit_eps) {
483 hit = true;
484 hit_idx = h.body_idx;
485 break;
486 }
487 t = t + max(h.dist, min_step);
488 if (t > max_t) { break; }
489 }
490
491 if (!hit) {
492 discard;
493 return Shaded(vec4<f32>(0.0, 0.0, 0.0, 0.0), 0.0);
494 }
495
496 let p_hit = ro + rd * t;
497 if (hit_idx == NO_BODY) {
498 if (loam_scene_at(p_hit).kind == LOAM_PRIM_HALFSPACE4D) {
499 discard;
500 return Shaded(vec4<f32>(0.0, 0.0, 0.0, 0.0), 0.0);
501 }
502 }
503 let n = estimate_normal(p_hit, hit_idx);
504 let light_dir = normalize(vec3<f32>(0.5, 0.85, 0.3));
505 let lambert = max(dot(n, light_dir), 0.0);
506 let ambient = 0.20;
507 var base = vec3<f32>(0.65, 0.65, 0.72);
508 if (hit_idx != NO_BODY) {
509 base = body_at(hit_idx).color;
510 }
511 let lit = base * (ambient + lambert * 0.85);
512 let fog = 1.0 - exp(-t * 0.05);
513 let final_color = mix(lit, sky(rd, u.sky_below, u.sky_above), fog * 0.5);
514 let image = vec3<f32>(0.0, 0.0, -t * dot(rd, u.camera_forward));
515 return Shaded(vec4<f32>(final_color, 1.0), loam_projective_depth(image, u.near));
516}
517
518@fragment
519fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> {
520 return shade(frag_pos).color;
521}
522
523@fragment
524fn fs_depth(@builtin(position) frag_pos: vec4<f32>) -> Fragment {
525 let shaded = shade(frag_pos);
526 return Fragment(shaded.color, shaded.depth);
527}
528"#
529);
530
531const HYPERSLICE_UNIFORMS_SIZE: u64 = std::mem::size_of::<Hyperslice4DUniforms>() as u64;
532
533fn strip_cell_stride(min_uniform_buffer_offset_alignment: u64) -> u64 {
534 HYPERSLICE_UNIFORMS_SIZE.div_ceil(min_uniform_buffer_offset_alignment)
535 * min_uniform_buffer_offset_alignment
536}
537
538fn strip_cell_uniforms(
539 base: &Hyperslice4DUniforms,
540 viewport: crate::Viewport,
541 w_slice: f32,
542 slot: usize,
543) -> Hyperslice4DUniforms {
544 let mut cell = *base;
545 cell.body_count = 1.0;
546 cell.body_offset = slot as f32;
547 cell.w_slice = w_slice;
548 cell.resolution = viewport.resolution_f32();
549 cell.viewport_origin = [viewport.x as f32, viewport.y as f32];
550 cell
551}
552
553const BODY_SIZE: u64 = std::mem::size_of::<BodyUniform>() as u64;
554
555fn body_buffer(device: &Device, label: &'static str, capacity: usize) -> Buffer {
556 device.create_buffer(&BufferDescriptor {
557 label: Some(label),
558 size: BODY_SIZE * capacity.max(1) as u64,
559 usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
560 mapped_at_creation: false,
561 })
562}
563
564fn body_bind_group(
565 device: &Device,
566 layout: &BindGroupLayout,
567 uniforms: BindingResource<'_>,
568 bodies: &Buffer,
569) -> BindGroup {
570 device.create_bind_group(&BindGroupDescriptor {
571 label: Some("hyperslice4d bg"),
572 layout,
573 entries: &[
574 BindGroupEntry {
575 binding: 0,
576 resource: uniforms,
577 },
578 BindGroupEntry {
579 binding: 1,
580 resource: bodies.as_entire_binding(),
581 },
582 ],
583 })
584}
585
586struct StripCellUniforms {
587 buffer: Buffer,
588 bodies: Buffer,
589 bind_groups: Vec<BindGroup>,
590 stride: u64,
591}
592
593impl StripCellUniforms {
594 fn new(device: &Device, layout: &BindGroupLayout, cell_count: usize) -> Self {
595 let stride = strip_cell_stride(device.limits().min_uniform_buffer_offset_alignment as u64);
596 let buffer = device.create_buffer(&BufferDescriptor {
597 label: Some("hyperslice4d strip cell uniforms"),
598 size: stride * cell_count as u64,
599 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
600 mapped_at_creation: false,
601 });
602 let bodies = body_buffer(device, "hyperslice4d strip cell bodies", cell_count);
603 let bind_groups = (0..cell_count)
604 .map(|i| {
605 body_bind_group(
606 device,
607 layout,
608 BindingResource::Buffer(BufferBinding {
609 buffer: &buffer,
610 offset: i as u64 * stride,
611 size: BufferSize::new(HYPERSLICE_UNIFORMS_SIZE),
612 }),
613 &bodies,
614 )
615 })
616 .collect();
617 Self {
618 buffer,
619 bodies,
620 bind_groups,
621 stride,
622 }
623 }
624}
625
626pub struct Hyperslice4DNode {
627 device: Device,
628 pipeline: RenderPipeline,
629 strip_pipeline: Option<RenderPipeline>,
630 uniforms: Hyperslice4DUniforms,
631 uniform_buf: Buffer,
632 bodies: Vec<BodyUniform>,
633 body_buf: Buffer,
634 body_capacity: usize,
635 bind_group: BindGroup,
636 bind_group_layout: BindGroupLayout,
637 clear_color: Color,
638 strip_cells: Option<StripCellUniforms>,
639 has_depth: bool,
640}
641
642impl Hyperslice4DNode {
643 pub fn new(device: &Device, surface_format: TextureFormat, module: &ShaderModule) -> Self {
644 Self::with_depth(device, surface_format, module, crate::DepthMode::Off)
645 }
646
647 pub fn with_depth(
648 device: &Device,
649 surface_format: TextureFormat,
650 module: &ShaderModule,
651 depth: crate::DepthMode,
652 ) -> Self {
653 let uniform_buf = device.create_buffer(&BufferDescriptor {
654 label: Some("hyperslice4d uniforms"),
655 size: std::mem::size_of::<Hyperslice4DUniforms>() as u64,
656 usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
657 mapped_at_creation: false,
658 });
659
660 let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
661 label: Some("hyperslice4d bgl"),
662 entries: &[
663 BindGroupLayoutEntry {
664 binding: 0,
665 visibility: ShaderStages::FRAGMENT,
666 ty: BindingType::Buffer {
667 ty: BufferBindingType::Uniform,
668 has_dynamic_offset: false,
669 min_binding_size: None,
670 },
671 count: None,
672 },
673 BindGroupLayoutEntry {
674 binding: 1,
675 visibility: ShaderStages::FRAGMENT,
676 ty: BindingType::Buffer {
677 ty: BufferBindingType::Storage { read_only: true },
678 has_dynamic_offset: false,
679 min_binding_size: None,
680 },
681 count: None,
682 },
683 ],
684 });
685
686 let body_buf = body_buffer(device, "hyperslice4d bodies", INITIAL_BODY_CAPACITY);
687 let bind_group = body_bind_group(device, &bgl, uniform_buf.as_entire_binding(), &body_buf);
688
689 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
690 label: Some("hyperslice4d pipeline layout"),
691 bind_group_layouts: &[&bgl],
692 push_constant_ranges: &[],
693 });
694
695 let pipeline = hyperslice_pipeline(device, &pipeline_layout, module, surface_format, depth);
696 let strip_pipeline = depth.is_active().then(|| {
697 hyperslice_pipeline(
698 device,
699 &pipeline_layout,
700 module,
701 surface_format,
702 crate::DepthMode::Off,
703 )
704 });
705
706 Self {
707 device: device.clone(),
708 pipeline,
709 strip_pipeline,
710 uniforms: Hyperslice4DUniforms::default(),
711 uniform_buf,
712 bodies: Vec::with_capacity(INITIAL_BODY_CAPACITY),
713 body_buf,
714 body_capacity: INITIAL_BODY_CAPACITY,
715 bind_group,
716 bind_group_layout: bgl,
717 clear_color: crate::sky_ground::SKY_HORIZON,
718 strip_cells: None,
719 has_depth: depth.is_active(),
720 }
721 }
722
723 pub fn uniforms(&self) -> &Hyperslice4DUniforms {
724 &self.uniforms
725 }
726 pub fn uniforms_mut(&mut self) -> &mut Hyperslice4DUniforms {
727 &mut self.uniforms
728 }
729
730 pub fn set_uniforms(&mut self, queue: &Queue, uniforms: Hyperslice4DUniforms) {
731 self.uniforms = uniforms;
732 queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&self.uniforms));
733 }
734
735 pub fn flush_uniforms(&self, queue: &Queue) {
736 queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&self.uniforms));
737 if !self.bodies.is_empty() {
738 queue.write_buffer(&self.body_buf, 0, bytemuck::cast_slice(&self.bodies));
739 }
740 }
741
742 pub fn set_clear_color(&mut self, color: Color) {
743 self.clear_color = color;
744 }
745
746 fn color_load_op(&self, load: ColorLoad) -> LoadOp<Color> {
747 match load {
748 ColorLoad::Clear => LoadOp::Clear(self.clear_color),
749 ColorLoad::Load => LoadOp::Load,
750 }
751 }
752
753 pub fn body_capacity(&self) -> usize {
754 self.body_capacity
755 }
756
757 pub fn set_bodies(&mut self, bodies: &[BodyUniform]) {
759 if bodies.len() > self.body_capacity {
760 let mut capacity = self.body_capacity.max(1);
761 while capacity < bodies.len() {
762 capacity *= 2;
763 }
764 self.body_buf = body_buffer(&self.device, "hyperslice4d bodies", capacity);
765 self.body_capacity = capacity;
766 self.bind_group = body_bind_group(
767 &self.device,
768 &self.bind_group_layout,
769 self.uniform_buf.as_entire_binding(),
770 &self.body_buf,
771 );
772 }
773 self.bodies.clear();
774 self.bodies.extend_from_slice(bodies);
775 self.uniforms.body_count = bodies.len() as f32;
776 self.uniforms.body_offset = 0.0;
777 }
778}
779
780impl Hyperslice4DNode {
781 pub fn record_in_viewport(
783 &mut self,
784 encoder: &mut wgpu::CommandEncoder,
785 view: &wgpu::TextureView,
786 viewport: crate::Viewport,
787 ) {
788 self.record(encoder, view, None, viewport, ColorLoad::Load);
789 }
790
791 pub fn record(
792 &self,
793 encoder: &mut wgpu::CommandEncoder,
794 view: &wgpu::TextureView,
795 depth_view: Option<&wgpu::TextureView>,
796 viewport: crate::Viewport,
797 load: ColorLoad,
798 ) {
799 match (self.has_depth, depth_view.is_some()) {
800 (true, false) => panic!(
801 "Hyperslice4DNode::record: the pipeline has a depth format but no depth view \
802 was given"
803 ),
804 (false, true) => panic!(
805 "Hyperslice4DNode::record: the pipeline has no depth format but a depth view \
806 was given"
807 ),
808 _ => {}
809 }
810 let depth_stencil_attachment = depth_view.map(|dv| RenderPassDepthStencilAttachment {
811 view: dv,
812 depth_ops: Some(Operations {
813 load: LoadOp::Load,
814 store: StoreOp::Store,
815 }),
816 stencil_ops: None,
817 });
818 let mut rp = encoder.begin_render_pass(&RenderPassDescriptor {
819 label: Some("hyperslice4d pass"),
820 color_attachments: &[Some(RenderPassColorAttachment {
821 view,
822 depth_slice: None,
823 resolve_target: None,
824 ops: Operations {
825 load: self.color_load_op(load),
826 store: StoreOp::Store,
827 },
828 })],
829 depth_stencil_attachment,
830 timestamp_writes: None,
831 occlusion_query_set: None,
832 });
833 viewport.apply(&mut rp);
834 rp.set_pipeline(&self.pipeline);
835 rp.set_bind_group(0, &self.bind_group, &[]);
836 rp.draw(0..3, 0..1);
837 }
838
839 pub fn record_strip(
841 &mut self,
842 device: &Device,
843 queue: &Queue,
844 encoder: &mut CommandEncoder,
845 view: &wgpu::TextureView,
846 cells: &[(crate::Viewport, f32, BodyUniform)],
847 load: ColorLoad,
848 ) -> Result<()> {
849 let drawn = cells
850 .iter()
851 .filter(|(viewport, _, _)| viewport.width != 0 && viewport.height != 0)
852 .count();
853 if drawn == 0 {
854 return Ok(());
855 }
856 if self
857 .strip_cells
858 .as_ref()
859 .is_none_or(|strip| strip.bind_groups.len() < drawn)
860 {
861 self.strip_cells = Some(StripCellUniforms::new(
862 device,
863 &self.bind_group_layout,
864 drawn,
865 ));
866 }
867 let strip = self
868 .strip_cells
869 .as_ref()
870 .context("missing strip uniform storage")?;
871
872 {
873 let upload_size = BufferSize::new(drawn as u64 * strip.stride)
874 .context("empty strip uniform storage")?;
875 let mut staging = queue
876 .write_buffer_with(&strip.buffer, 0, upload_size)
877 .context("mapping the filmstrip's per-cell uniform staging buffer")?;
878 let mut slot = 0usize;
879 for (viewport, w_slice, _) in cells {
880 if viewport.width == 0 || viewport.height == 0 {
881 continue;
882 }
883 let cell = strip_cell_uniforms(&self.uniforms, *viewport, *w_slice, slot);
884 let start = slot * strip.stride as usize;
885 staging[start..start + HYPERSLICE_UNIFORMS_SIZE as usize]
886 .copy_from_slice(bytemuck::bytes_of(&cell));
887 slot += 1;
888 }
889 }
890
891 {
892 let upload_size =
893 BufferSize::new(drawn as u64 * BODY_SIZE).context("empty strip body storage")?;
894 let mut staging = queue
895 .write_buffer_with(&strip.bodies, 0, upload_size)
896 .context("mapping the filmstrip's per-cell body staging buffer")?;
897 let mut slot = 0usize;
898 for (viewport, _, body) in cells {
899 if viewport.width == 0 || viewport.height == 0 {
900 continue;
901 }
902 let start = slot * BODY_SIZE as usize;
903 staging[start..start + BODY_SIZE as usize]
904 .copy_from_slice(bytemuck::bytes_of(body));
905 slot += 1;
906 }
907 }
908
909 {
910 let mut rp = encoder.begin_render_pass(&RenderPassDescriptor {
911 label: Some("hyperslice4d strip pass"),
912 color_attachments: &[Some(RenderPassColorAttachment {
913 view,
914 depth_slice: None,
915 resolve_target: None,
916 ops: Operations {
917 load: self.color_load_op(load),
918 store: StoreOp::Store,
919 },
920 })],
921 depth_stencil_attachment: None,
922 timestamp_writes: None,
923 occlusion_query_set: None,
924 });
925 rp.set_pipeline(self.strip_pipeline.as_ref().unwrap_or(&self.pipeline));
926 let mut slot = 0usize;
927 for (viewport, _, _) in cells {
928 if viewport.width == 0 || viewport.height == 0 {
929 continue;
930 }
931 viewport.apply(&mut rp);
932 rp.set_bind_group(0, &strip.bind_groups[slot], &[]);
933 rp.draw(0..3, 0..1);
934 slot += 1;
935 }
936 }
937 Ok(())
938 }
939}
940
941fn hyperslice_pipeline(
942 device: &Device,
943 layout: &PipelineLayout,
944 module: &ShaderModule,
945 surface_format: TextureFormat,
946 depth: crate::DepthMode,
947) -> RenderPipeline {
948 device.create_render_pipeline(&RenderPipelineDescriptor {
949 label: Some("hyperslice4d pipeline"),
950 layout: Some(layout),
951 vertex: VertexState {
952 module,
953 entry_point: Some("vs_fullscreen"),
954 buffers: &[],
955 compilation_options: Default::default(),
956 },
957 fragment: Some(FragmentState {
958 module,
959 entry_point: Some(if depth.is_active() {
960 "fs_depth"
961 } else {
962 "fs_main"
963 }),
964 targets: &[Some(ColorTargetState {
965 format: surface_format,
966 blend: None,
967 write_mask: ColorWrites::ALL,
968 })],
969 compilation_options: Default::default(),
970 }),
971 primitive: PrimitiveState {
972 topology: PrimitiveTopology::TriangleList,
973 ..Default::default()
974 },
975 depth_stencil: depth.format().map(|format| DepthStencilState {
976 format,
977 depth_write_enabled: depth.writes(),
978 depth_compare: crate::view::DEPTH_COMPARE,
979 stencil: StencilState::default(),
980 bias: DepthBiasState::default(),
981 }),
982 multisample: MultisampleState {
983 count: 1,
984 ..Default::default()
985 },
986 multiview: None,
987 cache: None,
988 })
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn kernel_validates_with_real_scene_union() {
997 use crate::raymarch::scene::{Scene4, SceneNode4};
998 use glam::Vec4;
999
1000 let scene = Scene4::new(
1001 SceneNode4::hypersphere(Vec4::new(-0.6, 0.7, -1.5, 0.0), 0.7)
1002 .union(SceneNode4::hypersphere(Vec4::new(0.6, 0.7, -1.5, 0.0), 0.7))
1003 .union(SceneNode4::hypersphere(Vec4::new(0.0, 1.0, 1.5, 0.0), 1.0))
1004 .union(SceneNode4::halfspace(Vec4::Y, 0.0)),
1005 );
1006 let scene_wgsl = scene.to_hyperslice_wgsl("u.w_slice");
1007 let polytope = super::super::polytope_data::polytope_extended_sdfs_wgsl();
1008 let source = format!("{HYPERSLICE_KERNEL_WGSL}\n{polytope}\n{scene_wgsl}");
1009 let module = naga::front::wgsl::parse_str(&source)
1010 .expect("hyperslice4d kernel + Scene4 emit should parse as WGSL");
1011 let flags = naga::valid::ValidationFlags::all();
1012 let caps = naga::valid::Capabilities::empty();
1013 naga::valid::Validator::new(flags, caps)
1014 .validate(&module)
1015 .expect("hyperslice4d kernel + Scene4 emit should validate");
1016 }
1017
1018 #[test]
1019 fn kernel_validates_with_gated_scene() {
1020 use crate::raymarch::scene::{Scene4, SceneNode4};
1021 use glam::Vec4;
1022
1023 let scene = Scene4::new(
1024 SceneNode4::hypersphere(Vec4::new(0.0, 1.0, 0.0, 0.0), 0.5)
1025 .union(SceneNode4::halfspace(Vec4::Y, 0.0)),
1026 );
1027 let scene_wgsl = scene.to_hyperslice_wgsl_gated("u.w_slice", "u.params.x");
1028 let polytope = super::super::polytope_data::polytope_extended_sdfs_wgsl();
1029 let source = format!("{HYPERSLICE_KERNEL_WGSL}\n{polytope}\n{scene_wgsl}");
1030 let module = naga::front::wgsl::parse_str(&source)
1031 .expect("gated Scene4 emit should parse against the kernel");
1032 let flags = naga::valid::ValidationFlags::all();
1033 let caps = naga::valid::Capabilities::empty();
1034 naga::valid::Validator::new(flags, caps)
1035 .validate(&module)
1036 .expect("gated Scene4 emit should validate against the kernel");
1037 }
1038
1039 fn strip_probe_cells() -> Vec<(crate::Viewport, f32, BodyUniform)> {
1040 crate::Viewport::full([192, 64])
1041 .split_horizontal(3)
1042 .zip([0.0_f32, 0.6, 0.9])
1043 .map(|(viewport, w_slice)| {
1044 (
1045 viewport,
1046 w_slice,
1047 BodyUniform::sphere([0.0; 4], 1.0, [1.0, 0.0, 0.0]),
1048 )
1049 })
1050 .collect()
1051 }
1052
1053 #[test]
1054 fn strip_cell_stride_is_offset_legal_and_covers_one_image() {
1055 for alignment in [32_u64, 64, 128, 256] {
1056 let stride = strip_cell_stride(alignment);
1057 assert_eq!(stride % alignment, 0, "stride must be an aligned offset");
1058 assert!(
1059 stride >= HYPERSLICE_UNIFORMS_SIZE,
1060 "stride {stride} would overlap the next cell's image",
1061 );
1062 assert!(
1063 stride - HYPERSLICE_UNIFORMS_SIZE < alignment,
1064 "stride {stride} wastes a whole alignment unit",
1065 );
1066 }
1067 }
1068
1069 #[test]
1070 fn kernel_uniforms_match_the_host_layout() {
1071 let source = format!(
1072 "{HYPERSLICE_KERNEL_WGSL}\n{}\n{EMPTY_SCENE_STUB}",
1073 super::super::polytope_data::polytope_stub_sdfs_wgsl()
1074 );
1075 let module = naga::front::wgsl::parse_str(&source).expect("parse");
1076 let ty = module
1077 .types
1078 .iter()
1079 .map(|(_, t)| t)
1080 .find(|t| t.name.as_deref() == Some("Uniforms"))
1081 .expect("the kernel declares `Uniforms`");
1082 let naga::TypeInner::Struct { members, span } = &ty.inner else {
1083 panic!("`Uniforms` is not a struct");
1084 };
1085 assert_eq!(*span as usize, std::mem::size_of::<Hyperslice4DUniforms>());
1086 let rust_offsets = [
1087 (
1088 "camera_pos",
1089 std::mem::offset_of!(Hyperslice4DUniforms, camera_pos),
1090 ),
1091 (
1092 "camera_forward",
1093 std::mem::offset_of!(Hyperslice4DUniforms, camera_forward),
1094 ),
1095 (
1096 "camera_right",
1097 std::mem::offset_of!(Hyperslice4DUniforms, camera_right),
1098 ),
1099 (
1100 "camera_up",
1101 std::mem::offset_of!(Hyperslice4DUniforms, camera_up),
1102 ),
1103 (
1104 "fov_y_tan",
1105 std::mem::offset_of!(Hyperslice4DUniforms, fov_y_tan),
1106 ),
1107 (
1108 "resolution",
1109 std::mem::offset_of!(Hyperslice4DUniforms, resolution),
1110 ),
1111 ("time", std::mem::offset_of!(Hyperslice4DUniforms, time)),
1112 ("tick", std::mem::offset_of!(Hyperslice4DUniforms, tick)),
1113 (
1114 "w_slice",
1115 std::mem::offset_of!(Hyperslice4DUniforms, w_slice),
1116 ),
1117 (
1118 "body_count",
1119 std::mem::offset_of!(Hyperslice4DUniforms, body_count),
1120 ),
1121 (
1122 "viewport_origin",
1123 std::mem::offset_of!(Hyperslice4DUniforms, viewport_origin),
1124 ),
1125 ("params", std::mem::offset_of!(Hyperslice4DUniforms, params)),
1126 ("near", std::mem::offset_of!(Hyperslice4DUniforms, near)),
1127 (
1128 "body_offset",
1129 std::mem::offset_of!(Hyperslice4DUniforms, body_offset),
1130 ),
1131 (
1132 "sky_below",
1133 std::mem::offset_of!(Hyperslice4DUniforms, sky_below),
1134 ),
1135 (
1136 "sky_above",
1137 std::mem::offset_of!(Hyperslice4DUniforms, sky_above),
1138 ),
1139 ];
1140 assert_eq!(members.len(), rust_offsets.len());
1141 for (member, (name, offset)) in members.iter().zip(rust_offsets) {
1142 assert_eq!(member.name.as_deref(), Some(name));
1143 assert_eq!(member.offset as usize, offset, "offset of {name}");
1144 }
1145 }
1146
1147 const EMPTY_SCENE_STUB: &str = r#"
1148const LOAM_PRIM_HYPERSPHERE4D: u32 = 0u;
1149const LOAM_PRIM_HALFSPACE4D: u32 = 1u;
1150const LOAM_PRIM_OTHER: u32 = 255u;
1151struct LoamSceneHit { dist: f32, kind: u32 }
1152fn loam_scene_at(p: vec3<f32>) -> LoamSceneHit {
1153 return LoamSceneHit(1.0e9, LOAM_PRIM_OTHER);
1154}
1155fn loam_scene_sdf(p: vec3<f32>) -> f32 {
1156 return loam_scene_at(p).dist;
1157}
1158fn loam_scene_max_t(ro: vec3<f32>, rd: vec3<f32>) -> f32 {
1159 return 1.0e9;
1160}
1161"#;
1162
1163 async fn request_device() -> Result<(Device, Queue), String> {
1164 let instance = wgpu::Instance::default();
1165 let adapter = instance
1166 .request_adapter(&wgpu::RequestAdapterOptions {
1167 power_preference: wgpu::PowerPreference::LowPower,
1168 compatible_surface: None,
1169 force_fallback_adapter: false,
1170 })
1171 .await
1172 .map_err(|e| format!("request_adapter failed: {e}"))?;
1173 adapter
1174 .request_device(&wgpu::DeviceDescriptor {
1175 label: Some("hyperslice4d-strip"),
1176 required_features: wgpu::Features::empty(),
1177 required_limits: wgpu::Limits::default(),
1178 memory_hints: wgpu::MemoryHints::default(),
1179 trace: wgpu::Trace::Off,
1180 experimental_features: Default::default(),
1181 })
1182 .await
1183 .map_err(|e| format!("request_device failed: {e}"))
1184 }
1185
1186 fn read_back_rgba(
1187 device: &Device,
1188 queue: &Queue,
1189 texture: &Texture,
1190 size: [u32; 2],
1191 ) -> Vec<u8> {
1192 let bytes_per_row = size[0] * 4;
1193 assert_eq!(
1194 bytes_per_row % COPY_BYTES_PER_ROW_ALIGNMENT,
1195 0,
1196 "probe width chosen so no row padding is needed"
1197 );
1198 let readback = device.create_buffer(&BufferDescriptor {
1199 label: Some("hyperslice4d strip readback"),
1200 size: (bytes_per_row * size[1]) as u64,
1201 usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
1202 mapped_at_creation: false,
1203 });
1204 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
1205 label: Some("hyperslice4d strip readback encoder"),
1206 });
1207 encoder.copy_texture_to_buffer(
1208 TexelCopyTextureInfo {
1209 texture,
1210 mip_level: 0,
1211 origin: Origin3d::ZERO,
1212 aspect: TextureAspect::All,
1213 },
1214 TexelCopyBufferInfo {
1215 buffer: &readback,
1216 layout: TexelCopyBufferLayout {
1217 offset: 0,
1218 bytes_per_row: Some(bytes_per_row),
1219 rows_per_image: None,
1220 },
1221 },
1222 Extent3d {
1223 width: size[0],
1224 height: size[1],
1225 depth_or_array_layers: 1,
1226 },
1227 );
1228 queue.submit(Some(encoder.finish()));
1229 let slice = readback.slice(..);
1230 slice.map_async(MapMode::Read, |_| {});
1231 device
1232 .poll(PollType::Wait {
1233 submission_index: None,
1234 timeout: None,
1235 })
1236 .expect("readback poll");
1237 let data = slice.get_mapped_range().to_vec();
1238 readback.unmap();
1239 data
1240 }
1241
1242 fn strip_probe_module(device: &Device) -> ShaderModule {
1243 let polytope = super::super::polytope_data::polytope_stub_sdfs_wgsl();
1244 device.create_shader_module(ShaderModuleDescriptor {
1245 label: Some("hyperslice4d strip probe"),
1246 source: ShaderSource::Wgsl(
1247 format!("{HYPERSLICE_KERNEL_WGSL}\n{polytope}\n{EMPTY_SCENE_STUB}").into(),
1248 ),
1249 })
1250 }
1251
1252 #[test]
1253 #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
1254 fn each_strip_cell_renders_its_own_w_slice_gpu_probe() {
1255 const SIZE: [u32; 2] = [192, 64];
1256 let (device, queue) = pollster::block_on(request_device()).expect("wgpu device");
1257
1258 let module = strip_probe_module(&device);
1259 let target = device.create_texture(&TextureDescriptor {
1260 label: Some("hyperslice4d strip probe target"),
1261 size: Extent3d {
1262 width: SIZE[0],
1263 height: SIZE[1],
1264 depth_or_array_layers: 1,
1265 },
1266 mip_level_count: 1,
1267 sample_count: 1,
1268 dimension: TextureDimension::D2,
1269 format: TextureFormat::Rgba8Unorm,
1270 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
1271 view_formats: &[],
1272 });
1273 let view = target.create_view(&TextureViewDescriptor::default());
1274
1275 let cells = strip_probe_cells();
1276 let mut node = Hyperslice4DNode::new(&device, TextureFormat::Rgba8Unorm, &module);
1277 let mut encoder = device.create_command_encoder(&Default::default());
1278 node.record_strip(
1279 &device,
1280 &queue,
1281 &mut encoder,
1282 &view,
1283 &cells,
1284 ColorLoad::Clear,
1285 )
1286 .expect("filmstrip should render");
1287 queue.submit(Some(encoder.finish()));
1288
1289 let pixels = read_back_rgba(&device, &queue, &target, SIZE);
1290 let footprints: Vec<usize> = cells
1291 .iter()
1292 .map(|(viewport, _, _)| {
1293 let mut covered = 0;
1294 for y in viewport.y..viewport.y + viewport.height {
1295 for x in viewport.x..viewport.x + viewport.width {
1296 let px = ((y * SIZE[0] + x) * 4) as usize;
1297 if pixels[px] > pixels[px + 2] {
1298 covered += 1;
1299 }
1300 }
1301 }
1302 covered
1303 })
1304 .collect();
1305
1306 assert!(
1307 footprints[2] > 0,
1308 "the w = 0.9 slice should still show a body: {footprints:?}"
1309 );
1310 assert!(
1311 footprints[0] > footprints[1] && footprints[1] > footprints[2],
1312 "footprints should shrink with |w|; equal cells mean one uniform image \
1313 fed every cell: {footprints:?}"
1314 );
1315 }
1316
1317 #[test]
1318 fn set_bodies_grows_past_the_old_thirty_two_body_ceiling() {
1319 let gpu = crate::device::noop_context();
1320 let module = strip_probe_module(&gpu.device);
1321 let mut node = Hyperslice4DNode::new(&gpu.device, TextureFormat::Rgba8Unorm, &module);
1322 let bodies = vec![BodyUniform::sphere([0.0; 4], 1.0, [1.0; 3]); 33];
1323 node.set_bodies(&bodies);
1324 assert_eq!(node.uniforms().body_count, 33.0);
1325 assert!(node.body_capacity() >= 33);
1326 }
1327
1328 const DEPTH_PROBE_SIZE: u32 = 64;
1329 const DEPTH_PROBE_NEAR: f32 = 0.05;
1330 const DEPTH_PROBE_CENTER: [f32; 4] = [0.0, 0.0, -3.0, 0.0];
1331 const DEPTH_PROBE_RADIUS: f32 = 0.5;
1332
1333 fn depth_probe_ray() -> glam::Vec3 {
1334 let pixel = DEPTH_PROBE_SIZE / 2;
1335 let uv = |v: u32| ((v as f32 + 0.5) / DEPTH_PROBE_SIZE as f32) * 2.0 - 1.0;
1336 let tan = Hyperslice4DUniforms::default().fov_y_tan;
1337 glam::Vec3::new(uv(pixel) * tan, -uv(pixel) * tan, -1.0).normalize()
1338 }
1339
1340 fn depth_probe_expected() -> f32 {
1341 let center = glam::Vec3::new(
1342 DEPTH_PROBE_CENTER[0],
1343 DEPTH_PROBE_CENTER[1],
1344 DEPTH_PROBE_CENTER[2],
1345 );
1346 let rd = depth_probe_ray();
1347 let along = rd.dot(center);
1348 let gap = along * along - center.length_squared() + DEPTH_PROBE_RADIUS * DEPTH_PROBE_RADIUS;
1349 assert!(gap > 0.0, "the probe ray misses the body");
1350 let t = along - gap.sqrt();
1351 crate::view::projective_depth(rd * t, DEPTH_PROBE_NEAR)
1352 }
1353
1354 #[test]
1355 #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
1356 fn the_thirty_third_body_still_shades_a_pixel_gpu_probe() {
1357 const SIZE: u32 = 64;
1358 let (device, queue) = pollster::block_on(request_device()).expect("wgpu device");
1359 let module = strip_probe_module(&device);
1360 let target = device.create_texture(&TextureDescriptor {
1361 label: Some("hyperslice4d 33-body probe"),
1362 size: Extent3d {
1363 width: SIZE,
1364 height: SIZE,
1365 depth_or_array_layers: 1,
1366 },
1367 mip_level_count: 1,
1368 sample_count: 1,
1369 dimension: TextureDimension::D2,
1370 format: TextureFormat::Rgba8Unorm,
1371 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
1372 view_formats: &[],
1373 });
1374 let view = target.create_view(&TextureViewDescriptor::default());
1375
1376 let mut bodies = vec![BodyUniform::default(); 32];
1377 bodies.push(BodyUniform::sphere(
1378 [0.0, 0.0, -3.0, 0.0],
1379 0.9,
1380 [1.0, 0.0, 0.0],
1381 ));
1382 let mut node = Hyperslice4DNode::new(&device, TextureFormat::Rgba8Unorm, &module);
1383 node.set_uniforms(
1384 &queue,
1385 Hyperslice4DUniforms {
1386 camera_pos: [0.0; 3],
1387 resolution: [SIZE as f32; 2],
1388 ..Hyperslice4DUniforms::default()
1389 },
1390 );
1391 node.set_bodies(&bodies);
1392 node.flush_uniforms(&queue);
1393
1394 let mut encoder = device.create_command_encoder(&Default::default());
1395 {
1396 encoder.begin_render_pass(&RenderPassDescriptor {
1397 label: Some("hyperslice4d 33-body clear"),
1398 color_attachments: &[Some(RenderPassColorAttachment {
1399 view: &view,
1400 depth_slice: None,
1401 resolve_target: None,
1402 ops: Operations {
1403 load: LoadOp::Clear(Color::BLACK),
1404 store: StoreOp::Store,
1405 },
1406 })],
1407 depth_stencil_attachment: None,
1408 timestamp_writes: None,
1409 occlusion_query_set: None,
1410 });
1411 }
1412 node.record(
1413 &mut encoder,
1414 &view,
1415 None,
1416 crate::Viewport::full([SIZE, SIZE]),
1417 ColorLoad::Load,
1418 );
1419 queue.submit(Some(encoder.finish()));
1420
1421 let pixels = read_back_rgba(&device, &queue, &target, [SIZE, SIZE]);
1422 let center = ((SIZE / 2 * SIZE + SIZE / 2) * 4) as usize;
1423 assert!(
1424 pixels[center] > pixels[center + 2],
1425 "the 33rd body must shade the centre pixel, not the background: {:?}",
1426 &pixels[center..center + 4]
1427 );
1428 }
1429
1430 #[test]
1431 #[ignore = "requires a working wgpu adapter; run with --include-ignored"]
1432 fn a_hyperslice_hit_writes_the_root_eyes_projective_depth_gpu_probe() {
1433 let size = Extent3d {
1434 width: DEPTH_PROBE_SIZE,
1435 height: DEPTH_PROBE_SIZE,
1436 depth_or_array_layers: 1,
1437 };
1438 let (device, queue) = pollster::block_on(request_device()).expect("wgpu device");
1439 let module = strip_probe_module(&device);
1440 let attachment = |format: TextureFormat, label: &str| {
1441 device.create_texture(&TextureDescriptor {
1442 label: Some(label),
1443 size,
1444 mip_level_count: 1,
1445 sample_count: 1,
1446 dimension: TextureDimension::D2,
1447 format,
1448 usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
1449 view_formats: &[],
1450 })
1451 };
1452 let color = attachment(TextureFormat::Rgba8Unorm, "hyperslice4d depth probe color");
1453 let depth = attachment(crate::view::DEPTH_FORMAT, "hyperslice4d depth probe depth");
1454 let color_view = color.create_view(&TextureViewDescriptor::default());
1455 let depth_view = depth.create_view(&TextureViewDescriptor::default());
1456
1457 let mut node = Hyperslice4DNode::with_depth(
1458 &device,
1459 TextureFormat::Rgba8Unorm,
1460 &module,
1461 crate::DepthMode::ReadWrite {
1462 format: crate::view::DEPTH_FORMAT,
1463 },
1464 );
1465 node.set_uniforms(
1466 &queue,
1467 Hyperslice4DUniforms {
1468 camera_pos: [0.0; 3],
1469 resolution: [DEPTH_PROBE_SIZE as f32; 2],
1470 near: DEPTH_PROBE_NEAR,
1471 ..Hyperslice4DUniforms::default()
1472 },
1473 );
1474 node.set_bodies(&[BodyUniform::sphere(
1475 DEPTH_PROBE_CENTER,
1476 DEPTH_PROBE_RADIUS,
1477 [1.0, 1.0, 1.0],
1478 )]);
1479 node.flush_uniforms(&queue);
1480
1481 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
1482 label: Some("hyperslice4d depth probe"),
1483 });
1484 {
1485 encoder.begin_render_pass(&RenderPassDescriptor {
1486 label: Some("hyperslice4d depth probe clear"),
1487 color_attachments: &[Some(RenderPassColorAttachment {
1488 view: &color_view,
1489 depth_slice: None,
1490 resolve_target: None,
1491 ops: Operations {
1492 load: LoadOp::Clear(Color::BLACK),
1493 store: StoreOp::Store,
1494 },
1495 })],
1496 depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
1497 view: &depth_view,
1498 depth_ops: Some(Operations {
1499 load: LoadOp::Clear(crate::view::DEPTH_CLEAR),
1500 store: StoreOp::Store,
1501 }),
1502 stencil_ops: None,
1503 }),
1504 timestamp_writes: None,
1505 occlusion_query_set: None,
1506 });
1507 }
1508 node.record(
1509 &mut encoder,
1510 &color_view,
1511 Some(&depth_view),
1512 crate::Viewport::full([DEPTH_PROBE_SIZE, DEPTH_PROBE_SIZE]),
1513 ColorLoad::Load,
1514 );
1515 let readback = device.create_buffer(&BufferDescriptor {
1516 label: Some("hyperslice4d depth probe readback"),
1517 size: (DEPTH_PROBE_SIZE * DEPTH_PROBE_SIZE * 4) as u64,
1518 usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
1519 mapped_at_creation: false,
1520 });
1521 encoder.copy_texture_to_buffer(
1522 TexelCopyTextureInfo {
1523 texture: &depth,
1524 mip_level: 0,
1525 origin: Origin3d::ZERO,
1526 aspect: TextureAspect::DepthOnly,
1527 },
1528 TexelCopyBufferInfo {
1529 buffer: &readback,
1530 layout: TexelCopyBufferLayout {
1531 offset: 0,
1532 bytes_per_row: Some(DEPTH_PROBE_SIZE * 4),
1533 rows_per_image: None,
1534 },
1535 },
1536 size,
1537 );
1538 queue.submit(Some(encoder.finish()));
1539 readback.slice(..).map_async(MapMode::Read, |_| {});
1540 device
1541 .poll(PollType::Wait {
1542 submission_index: None,
1543 timeout: None,
1544 })
1545 .expect("readback poll");
1546 let depths =
1547 bytemuck::cast_slice::<u8, f32>(&readback.slice(..).get_mapped_range()).to_vec();
1548
1549 let center = DEPTH_PROBE_SIZE / 2;
1550 let written = depths[(center * DEPTH_PROBE_SIZE + center) as usize];
1551 let expected = depth_probe_expected();
1552 assert!(
1553 (written - expected).abs() <= 5.0e-5,
1554 "the hit wrote depth {written}, not the projective depth {expected}"
1555 );
1556 }
1557}