1use crate::effects::ParticleInstance;
2use crate::flat::FlatScene;
3use crate::frame::{Matrix, Scene, multiply};
4use crate::renderer::{Effects, Finish, Text, invert};
5use crate::text::{PackedGlyph, TextSpace};
6use pfx_gpu::{GpuProfiler, wgpu};
7use std::sync::mpsc::{Receiver, TryRecvError};
8
9pub const SETTLE: u32 = crate::taa::CYCLE;
10pub const MARGIN: u32 = 4;
11pub const DEPTH_PROBES: usize = 16;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct Region {
15 pub x: u32,
16 pub y: u32,
17 pub width: u32,
18 pub height: u32,
19}
20
21impl Region {
22 pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
23 Self {
24 x,
25 y,
26 width,
27 height,
28 }
29 }
30
31 pub fn whole(width: u32, height: u32) -> Self {
32 Self::new(0, 0, width, height)
33 }
34
35 pub fn from_bounds(bounds: [f32; 4], width: u32, height: u32) -> Option<Self> {
36 if !bounds.iter().all(|value| value.is_finite()) {
37 return Some(Self::whole(width, height));
38 }
39 let left = bounds[0].floor().clamp(0.0, width as f32) as u32;
40 let top = bounds[1].floor().clamp(0.0, height as f32) as u32;
41 let right = bounds[2].ceil().clamp(0.0, width as f32) as u32;
42 let bottom = bounds[3].ceil().clamp(0.0, height as f32) as u32;
43 (right > left && bottom > top).then(|| Self::new(left, top, right - left, bottom - top))
44 }
45
46 pub fn right(&self) -> u32 {
47 self.x + self.width
48 }
49
50 pub fn bottom(&self) -> u32 {
51 self.y + self.height
52 }
53
54 pub fn area(&self) -> u64 {
55 u64::from(self.width) * u64::from(self.height)
56 }
57
58 pub fn is_empty(&self) -> bool {
59 self.width == 0 || self.height == 0
60 }
61
62 pub fn contains(&self, x: u32, y: u32) -> bool {
63 x >= self.x && y >= self.y && x < self.right() && y < self.bottom()
64 }
65
66 pub fn clip(self, width: u32, height: u32) -> Self {
67 let x = self.x.min(width);
68 let y = self.y.min(height);
69 Self::new(
70 x,
71 y,
72 self.right().min(width) - x,
73 self.bottom().min(height) - y,
74 )
75 }
76
77 pub fn grow(self, margin: u32, width: u32, height: u32) -> Self {
78 let x = self.x.saturating_sub(margin);
79 let y = self.y.saturating_sub(margin);
80 Self::new(
81 x,
82 y,
83 (self.right() + margin).min(width) - x,
84 (self.bottom() + margin).min(height) - y,
85 )
86 }
87
88 pub fn union(self, other: Self) -> Self {
89 let x = self.x.min(other.x);
90 let y = self.y.min(other.y);
91 Self::new(
92 x,
93 y,
94 self.right().max(other.right()) - x,
95 self.bottom().max(other.bottom()) - y,
96 )
97 }
98
99 pub fn touches(&self, other: &Self, gap: u32) -> bool {
100 self.x <= other.right() + gap
101 && other.x <= self.right() + gap
102 && self.y <= other.bottom() + gap
103 && other.y <= self.bottom() + gap
104 }
105}
106
107pub fn merge(regions: &[Region], width: u32, height: u32) -> Vec<Region> {
108 let mut merged: Vec<Region> = regions
109 .iter()
110 .map(|region| region.clip(width, height))
111 .filter(|region| !region.is_empty())
112 .collect();
113 loop {
114 let mut joined = false;
115 let mut index = 0;
116 while index < merged.len() {
117 let mut other = index + 1;
118 while other < merged.len() {
119 if merged[index].touches(&merged[other], 16) {
120 let region = merged.swap_remove(other);
121 merged[index] = merged[index].union(region);
122 joined = true;
123 } else {
124 other += 1;
125 }
126 }
127 index += 1;
128 }
129 if !joined {
130 break;
131 }
132 }
133 let covered: u64 = merged.iter().map(Region::area).sum();
134 if covered * 5 > Region::whole(width, height).area() * 3 {
135 return vec![Region::whole(width, height)];
136 }
137 merged.sort_by_key(|region| (region.y, region.x));
138 merged
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub enum Need {
143 None,
144 Reproject,
145 Partial(Vec<Region>),
146 Full,
147}
148
149impl Need {
150 pub fn draws(&self) -> bool {
151 !matches!(self, Self::None)
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum Shown {
157 Full,
158 Reproject,
159 Partial,
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct Limits {
164 pub angle: f32,
165 pub zoom: f32,
166 pub holes: f32,
167 pub frames: u32,
168 pub settle: u32,
169}
170
171impl Default for Limits {
172 fn default() -> Self {
173 Self {
174 angle: 0.6_f32.to_radians(),
175 zoom: 0.03,
176 holes: 0.005,
177 frames: 90,
178 settle: SETTLE,
179 }
180 }
181}
182
183pub struct Next<'a> {
184 pub scene: &'a Scene<'a>,
185 pub text: &'a Text<'a>,
186 pub effects: &'a Effects<'a>,
187 pub style: Finish,
188 pub hud: Option<&'a FlatScene<'a>>,
189 pub content: u64,
190 pub overlay: u64,
191 pub moving: &'a [Region],
192}
193
194impl<'a> Next<'a> {
195 pub fn new(
196 scene: &'a Scene<'a>,
197 text: &'a Text<'a>,
198 effects: &'a Effects<'a>,
199 style: Finish,
200 ) -> Self {
201 Self {
202 scene,
203 text,
204 effects,
205 style,
206 hud: None,
207 content: 0,
208 overlay: 0,
209 moving: &[],
210 }
211 }
212}
213
214#[derive(Clone, Copy, Debug, Default, PartialEq)]
215pub struct DemandStats {
216 pub shown: Option<Shown>,
217 pub since_full: u32,
218 pub settle: u32,
219 pub holes: f32,
220 pub depth: Option<f32>,
221}
222
223#[derive(Clone, Debug, PartialEq)]
224pub(crate) struct Summary {
225 pub view: Matrix,
226 pub projection: Matrix,
227 pub position: [f32; 3],
228 pub size: [u32; 2],
229 pub world: u64,
230 pub anchors: Vec<Matrix>,
231 pub overlay: u64,
232 pub ticking: bool,
233 pub layered: Vec<Region>,
234 pub moving: Vec<Region>,
235}
236
237impl Summary {
238 fn same_camera(&self, other: &Self) -> bool {
239 self.view == other.view && self.projection == other.projection
240 }
241
242 fn same_world(&self, other: &Self) -> bool {
243 self.world == other.world
244 && self.anchors.len() == other.anchors.len()
245 && self
246 .anchors
247 .iter()
248 .zip(&other.anchors)
249 .all(|(a, b)| close(a, b))
250 }
251}
252
253fn close(a: &Matrix, b: &Matrix) -> bool {
254 a.iter()
255 .flatten()
256 .zip(b.iter().flatten())
257 .all(|(x, y)| (x - y).abs() <= 1e-4 * x.abs().max(y.abs()).max(1.0))
258}
259
260#[derive(Clone, Debug, Default)]
261pub(crate) struct Book {
262 pub cached: Option<Summary>,
263 pub shown: Option<Summary>,
264 pub kind: Option<Shown>,
265 pub since_full: u32,
266 pub settle: u32,
267 pub holes: f32,
268 pub depth: Option<f32>,
269 pub still: bool,
270}
271
272#[derive(Clone, Copy, Debug, PartialEq)]
273pub struct Motion {
274 pub angle: f32,
275 pub zoom: f32,
276}
277
278fn rotation(view: &Matrix) -> [[f32; 3]; 3] {
279 std::array::from_fn(|column| std::array::from_fn(|row| view[column][row]))
280}
281
282pub fn motion(
283 from: (&Matrix, &Matrix, [f32; 3]),
284 to: (&Matrix, &Matrix, [f32; 3]),
285 depth: Option<f32>,
286) -> Option<Motion> {
287 let (view0, projection0, position0) = from;
288 let (view1, projection1, position1) = to;
289 if projection0[2][3] != -1.0 || projection0[3][3] != 0.0 {
290 return None;
291 }
292 let scale0 = projection0[1][1];
293 let scale1 = projection1[1][1];
294 if !(scale0 > 0.0 && scale1 > 0.0) {
295 return None;
296 }
297 let aspect0 = projection0[0][0] / scale0;
298 let aspect1 = projection1[0][0] / scale1;
299 if (aspect0 - aspect1).abs() > 1e-5 * aspect0.abs().max(1.0) {
300 return None;
301 }
302 for (column, row) in [
303 (2, 0),
304 (2, 1),
305 (2, 2),
306 (2, 3),
307 (3, 2),
308 (3, 3),
309 (3, 0),
310 (3, 1),
311 ] {
312 let a = projection0[column][row];
313 let b = projection1[column][row];
314 if (a - b).abs() > 1e-5 * a.abs().max(b.abs()).max(1.0) {
315 return None;
316 }
317 }
318 let r0 = rotation(view0);
319 let r1 = rotation(view1);
320 let relative: [[f64; 3]; 3] = std::array::from_fn(|i| {
321 std::array::from_fn(|j| {
322 (0..3)
323 .map(|k| f64::from(r1[k][i]) * f64::from(r0[k][j]))
324 .sum()
325 })
326 });
327 let trace = relative[0][0] + relative[1][1] + relative[2][2];
328 let skew = [
329 relative[2][1] - relative[1][2],
330 relative[0][2] - relative[2][0],
331 relative[1][0] - relative[0][1],
332 ];
333 let sine = (skew[0] * skew[0] + skew[1] * skew[1] + skew[2] * skew[2]).sqrt() * 0.5;
334 let turned = sine.atan2((trace - 1.0) * 0.5) as f32;
335 let shift = {
336 let d = [
337 position1[0] - position0[0],
338 position1[1] - position0[1],
339 position1[2] - position0[2],
340 ];
341 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
342 };
343 let shifted = if shift <= 1e-7 {
344 0.0
345 } else {
346 match depth {
347 Some(depth) if depth.is_finite() && depth > 0.0 => shift / depth,
348 Some(_) => 0.0,
349 None => return None,
350 }
351 };
352 Some(Motion {
353 angle: turned + shifted,
354 zoom: (scale1 / scale0).ln().abs(),
355 })
356}
357
358pub(crate) fn decide(book: &Book, next: &Summary, limits: &Limits) -> Need {
359 let Some(cached) = &book.cached else {
360 return Need::Full;
361 };
362 if next.size != cached.size
363 || !next.same_world(cached)
364 || next.ticking
365 || book.settle > 0
366 || (!next.layered.is_empty() && !book.still)
367 {
368 return Need::Full;
369 }
370 let shown = book.shown.as_ref().unwrap_or(cached);
371 let kind = book.kind.unwrap_or(Shown::Full);
372 let [width, height] = next.size;
373 let mut layered: Vec<Region> = next.layered.clone();
374 layered.extend(shown.layered.iter().copied());
375 let overlay_changed = next.overlay != shown.overlay;
376 if !next.same_camera(cached) {
377 let Some(moved) = motion(
378 (&cached.view, &cached.projection, cached.position),
379 (&next.view, &next.projection, next.position),
380 book.depth,
381 ) else {
382 return Need::Full;
383 };
384 if moved.angle > limits.angle
385 || moved.zoom > limits.zoom
386 || book.since_full >= limits.frames
387 || book.holes > limits.holes
388 {
389 return Need::Full;
390 }
391 if kind == Shown::Reproject
392 && next.same_camera(shown)
393 && layered.is_empty()
394 && !overlay_changed
395 && next.moving.is_empty()
396 {
397 return Need::Full;
398 }
399 return Need::Reproject;
400 }
401 if kind == Shown::Reproject {
402 return Need::Reproject;
403 }
404 let mut regions = layered;
405 regions.extend(next.moving.iter().copied());
406 regions.extend(shown.moving.iter().copied());
407 if overlay_changed {
408 regions.push(Region::whole(width, height));
409 }
410 if regions.is_empty() {
411 Need::None
412 } else {
413 Need::Partial(merge(®ions, width, height))
414 }
415}
416
417pub(crate) fn record(book: &mut Book, summary: Summary, kind: Shown, limits: &Limits) {
418 match kind {
419 Shown::Full => {
420 let changed = book
421 .cached
422 .as_ref()
423 .is_none_or(|cached| !cached.same_world(&summary) || cached.size != summary.size);
424 book.settle = if changed {
425 limits.settle
426 } else {
427 book.settle.saturating_sub(1)
428 };
429 book.cached = Some(summary.clone());
430 book.since_full = 0;
431 book.holes = 0.0;
432 }
433 Shown::Reproject => book.since_full = book.since_full.saturating_add(1),
434 Shown::Partial => {}
435 }
436 book.shown = Some(summary);
437 book.kind = Some(kind);
438}
439
440#[derive(Clone, Copy)]
441pub(crate) struct Print(u64);
442
443impl Print {
444 pub fn new() -> Self {
445 Self(0xcbf2_9ce4_8422_2325)
446 }
447
448 pub fn word(&mut self, value: u32) {
449 self.0 = (self.0 ^ u64::from(value)).wrapping_mul(0x0000_0100_0000_01b3);
450 }
451
452 pub fn wide(&mut self, value: u64) {
453 self.word(value as u32);
454 self.word((value >> 32) as u32);
455 }
456
457 pub fn float(&mut self, value: f32) {
458 self.word(value.to_bits());
459 }
460
461 pub fn floats(&mut self, values: &[f32]) {
462 for value in values {
463 self.float(*value);
464 }
465 }
466
467 pub fn matrix(&mut self, matrix: &Matrix) {
468 for column in matrix {
469 self.floats(column);
470 }
471 }
472
473 pub fn bytes(&mut self, bytes: &[u8]) {
474 let mut chunks = bytes.chunks_exact(4);
475 for chunk in &mut chunks {
476 self.word(u32::from_le_bytes(chunk.try_into().unwrap()));
477 }
478 for byte in chunks.remainder() {
479 self.word(u32::from(*byte) | 0x100);
480 }
481 self.word(bytes.len() as u32);
482 }
483
484 pub fn debug(&mut self, value: &impl std::fmt::Debug) {
485 use std::fmt::Write;
486 let _ = write!(self, "{value:?}");
487 self.word(0xffff_ffff);
488 }
489
490 pub fn finish(self) -> u64 {
491 self.0
492 }
493}
494
495impl std::fmt::Write for Print {
496 fn write_str(&mut self, text: &str) -> std::fmt::Result {
497 self.bytes(text.as_bytes());
498 Ok(())
499 }
500}
501
502pub(crate) fn print_scene(print: &mut Print, scene: &Scene<'_>) {
503 print.word(scene.seed);
504 print.floats(&scene.sun.direction);
505 print.floats(&scene.sun.colour);
506 print.float(scene.sun.intensity);
507 print.float(scene.wind.current.strength);
508 print.floats(&scene.wind.current.direction);
509 print.word(scene.instances.len() as u32);
510 for instance in scene.instances {
511 print.word(instance.mesh.0);
512 print.matrix(&instance.model);
513 print.word(instance.material);
514 print.word(instance.id);
515 print.float(instance.opacity);
516 print.float(instance.alpha_cutoff);
517 print.word(instance.deformer.0);
518 print.float(instance.age);
519 print.float(instance.coverage);
520 print.word(
521 u32::from(instance.shadow_only)
522 | u32::from(instance.casts_shadow) << 1
523 | u32::from(instance.two_sided) << 2,
524 );
525 }
526 print.word(scene.materials.len() as u32);
527 for material in scene.materials {
528 print.debug(material);
529 }
530 print.word(scene.deformers.len() as u32);
531 for deformer in scene.deformers {
532 print.debug(deformer);
533 }
534}
535
536fn print_space(
537 print: &mut Print,
538 space: &TextSpace,
539 anchors: &mut Vec<Matrix>,
540 view_projection: &Matrix,
541) {
542 match space {
543 TextSpace::Surface {
544 model_view_projection,
545 } => {
546 print.word(1);
547 anchors.push(anchor(view_projection, model_view_projection));
548 }
549 TextSpace::Deformed {
550 model,
551 layout,
552 normal,
553 lift,
554 deformer,
555 ..
556 } => {
557 print.word(2);
558 print.matrix(model);
559 print.matrix(layout);
560 print.floats(normal);
561 print.float(*lift);
562 print.word(deformer.0);
563 }
564 TextSpace::Overlay { width, height } => {
565 print.word(3);
566 print.word(*width);
567 print.word(*height);
568 }
569 TextSpace::LitSurface {
570 model_view_projection,
571 model,
572 } => {
573 print.word(4);
574 print.matrix(model);
575 anchors.push(anchor(view_projection, model_view_projection));
576 }
577 TextSpace::LitDeformed {
578 model,
579 layout,
580 normal,
581 lift,
582 deformer,
583 ..
584 } => {
585 print.word(5);
586 print.matrix(model);
587 print.matrix(layout);
588 print.floats(normal);
589 print.float(*lift);
590 print.word(deformer.0);
591 }
592 }
593}
594
595fn anchor(view_projection: &Matrix, model_view_projection: &Matrix) -> Matrix {
596 invert(*view_projection)
597 .map(|inverse| multiply(inverse, *model_view_projection))
598 .unwrap_or(*model_view_projection)
599}
600
601pub(crate) fn print_text(
602 world: &mut Print,
603 overlay: &mut Print,
604 anchors: &mut Vec<Matrix>,
605 text: &Text<'_>,
606 view_projection: &Matrix,
607) {
608 for (print, items) in [(&mut *world, &text.surface), (&mut *overlay, &text.overlay)] {
609 print.word(items.len() as u32);
610 for item in items {
611 print.wide(std::ptr::from_ref(item.atlas) as usize as u64);
612 print.floats(&item.color);
613 print.word(item.id);
614 print_space(print, &item.space, anchors, view_projection);
615 print.word(item.paragraph.glyphs.len() as u32);
616 for glyph in &item.paragraph.glyphs {
617 print.bytes(bytemuck::bytes_of(&PackedGlyph::from(glyph)));
618 }
619 }
620 }
621 for (print, items) in [
622 (&mut *world, &text.surface_quads),
623 (&mut *overlay, &text.overlay_quads),
624 ] {
625 print.word(items.len() as u32);
626 for item in items {
627 print.wide(std::ptr::from_ref(item.atlas) as usize as u64);
628 print.word(u32::from(item.icons));
629 print.word(item.id);
630 print_space(print, &item.space, anchors, view_projection);
631 print.word(item.quads.len() as u32);
632 for quad in item.quads {
633 print.bytes(bytemuck::bytes_of(&PackedGlyph::from(quad)));
634 }
635 }
636 }
637}
638
639pub(crate) fn print_effects(print: &mut Print, effects: &Effects<'_>) {
640 print.word(effects.glass.len() as u32);
641 for surface in effects.glass {
642 print.word(surface.mesh.0);
643 print.matrix(&surface.model);
644 print.debug(&surface.material);
645 print.word(u32::from(surface.liquid));
646 print.float(surface.fluid_height);
647 print.float(surface.ripple_height);
648 print.float(surface.caustic_strength);
649 print.debug(&surface.tinted);
650 print.word(surface.id);
651 print.word(u32::from(surface.casts_shadow) | u32::from(surface.shadow_only) << 1);
652 print.floats(surface.clip.as_flattened());
653 }
654 print.word(effects.props.len() as u32);
655 for prop in effects.props {
656 print.bytes(bytemuck::bytes_of(prop));
657 }
658}
659
660fn transform(matrix: &Matrix, point: [f32; 4]) -> [f32; 4] {
661 std::array::from_fn(|row| {
662 (0..4)
663 .map(|column| matrix[column][row] * point[column])
664 .sum()
665 })
666}
667
668pub fn box_region(
669 view_projection: &Matrix,
670 lo: [f32; 3],
671 hi: [f32; 3],
672 margin: u32,
673 width: u32,
674 height: u32,
675) -> Option<Region> {
676 let mut bounds = [
677 f32::INFINITY,
678 f32::INFINITY,
679 f32::NEG_INFINITY,
680 f32::NEG_INFINITY,
681 ];
682 for x in [lo[0], hi[0]] {
683 for y in [lo[1], hi[1]] {
684 for z in [lo[2], hi[2]] {
685 let clip = transform(view_projection, [x, y, z, 1.0]);
686 if clip[3] <= 0.0 || !clip.iter().all(|value| value.is_finite()) {
687 return Some(Region::whole(width, height));
688 }
689 let screen = [
690 (clip[0] / clip[3] * 0.5 + 0.5) * width as f32,
691 (0.5 - clip[1] / clip[3] * 0.5) * height as f32,
692 ];
693 bounds[0] = bounds[0].min(screen[0]);
694 bounds[1] = bounds[1].min(screen[1]);
695 bounds[2] = bounds[2].max(screen[0]);
696 bounds[3] = bounds[3].max(screen[1]);
697 }
698 }
699 }
700 let margin = margin as f32;
701 Region::from_bounds(
702 [
703 bounds[0] - margin,
704 bounds[1] - margin,
705 bounds[2] + margin,
706 bounds[3] + margin,
707 ],
708 width,
709 height,
710 )
711}
712
713pub fn particle_region(
714 view_projection: &Matrix,
715 particles: &[ParticleInstance],
716 margin: u32,
717 width: u32,
718 height: u32,
719) -> Option<Region> {
720 let mut lo = [f32::INFINITY; 3];
721 let mut hi = [f32::NEG_INFINITY; 3];
722 for particle in particles {
723 let reach = particle.position_size[3].abs() * 1.5;
724 for axis in 0..3 {
725 lo[axis] = lo[axis].min(particle.position_size[axis] - reach);
726 hi[axis] = hi[axis].max(particle.position_size[axis] + reach);
727 }
728 }
729 if particles.is_empty() {
730 return None;
731 }
732 box_region(view_projection, lo, hi, margin, width, height)
733}
734
735pub fn local_post(chain: &pfx_post::Chain) -> bool {
736 chain.is_local()
737}
738
739pub(crate) const WARP_WGSL: &str = r#"
740struct Warp {
741 across: vec4f,
742 down: vec4f,
743 corner: vec4f,
744 origin: vec4f,
745 size: vec4f,
746 limits: vec4f,
747}
748@group(0) @binding(0) var<uniform> warp: Warp;
749@group(0) @binding(1) var colour: texture_2d<f32>;
750@group(0) @binding(2) var depth: texture_2d<f32>;
751@group(0) @binding(3) var smooth_sampler: sampler;
752@group(0) @binding(4) var<storage, read_write> holes: array<atomic<u32>, 4>;
753@group(0) @binding(5) var tiles: texture_2d<f32>;
754@group(0) @binding(6) var overlay: texture_2d<f32>;
755
756@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
757 let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
758 return vec4f(xy[index], 0.0, 1.0);
759}
760
761fn depth_at(uv: vec2f) -> f32 {
762 let size = vec2i(warp.size.xy);
763 let p = clamp(vec2i(floor(uv * warp.size.xy)), vec2i(0), size - vec2i(1));
764 return textureLoad(depth, p, 0).r;
765}
766
767fn moved(uv: vec2f, d: f32) -> vec2f {
768 let toward = warp.across * uv.x + warp.down * uv.y + warp.corner;
769 let clip = select(toward * d + warp.origin, toward, d >= warp.limits.x);
770 let ndc = clip.xy / clip.w;
771 return vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
772}
773
774struct Found {
775 uv: vec2f,
776 miss: f32,
777 depth: f32,
778}
779
780fn solve(goal: vec2f, start: vec2f) -> Found {
781 var s = start;
782 var found: Found;
783 for (var step = 0; step < 4; step = step + 1) {
784 found.depth = depth_at(s);
785 let error = goal - moved(s, found.depth);
786 found.miss = length(error * warp.size.xy);
787 found.uv = s;
788 if (found.miss < 0.05) {
789 break;
790 }
791 s = s + error;
792 }
793 return found;
794}
795
796fn seed(goal: vec2f, d: f32) -> vec2f {
797 return goal - (moved(goal, d) - goal);
798}
799
800fn pick(goal: vec2f) -> Found {
801 let first = solve(goal, seed(goal, depth_at(goal)));
802 let last = vec2i(textureDimensions(tiles)) - vec2i(1);
803 let tile = textureLoad(tiles, clamp(vec2i(first.uv * warp.size.xy / warp.limits.z), vec2i(0), last), 0).rg;
804 let near = min(tile.x, first.depth);
805 let far = max(tile.y, first.depth);
806 let tolerance = warp.limits.y;
807 if (far <= near * 1.02 && first.miss < tolerance) {
808 return first;
809 }
810 let front = solve(goal, seed(goal, near));
811 let back = solve(goal, seed(goal, far));
812 var best = back;
813 var hit = back.miss < tolerance;
814 if (first.miss < tolerance && (!hit || first.depth < best.depth)) {
815 best = first;
816 hit = true;
817 }
818 if (front.miss < tolerance && (!hit || front.depth < best.depth)) {
819 best = front;
820 hit = true;
821 }
822 if (!hit) {
823 best.miss = 1.0e9;
824 }
825 return best;
826}
827
828fn fetch(uv: vec2f) -> vec4f {
829 return textureSampleLevel(colour, smooth_sampler, uv, 0.0);
830}
831
832fn catmull_rom(uv: vec2f) -> vec4f {
833 let size = warp.size.xy;
834 let inv = warp.size.zw;
835 let at = uv * size;
836 let centre = floor(at - 0.5) + 0.5;
837 let f = at - centre;
838 let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
839 let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
840 let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
841 let w3 = f * f * (-0.5 + 0.5 * f);
842 let w12 = w1 + w2;
843 let offset = w2 / w12;
844 let t0 = (centre - 1.0) * inv;
845 let t3 = (centre + 2.0) * inv;
846 let t12 = (centre + offset) * inv;
847 var sum = vec4f(0.0);
848 var weight = 0.0;
849 let a = w12.x * w0.y;
850 let ta = fetch(vec2f(t12.x, t0.y));
851 sum += ta * a;
852 let b = w0.x * w12.y;
853 let tb = fetch(vec2f(t0.x, t12.y));
854 sum += tb * b;
855 let c = w12.x * w12.y;
856 let tc = fetch(vec2f(t12.x, t12.y));
857 sum += tc * c;
858 let d = w3.x * w12.y;
859 let td = fetch(vec2f(t3.x, t12.y));
860 sum += td * d;
861 let e = w12.x * w3.y;
862 let te = fetch(vec2f(t12.x, t3.y));
863 sum += te * e;
864 weight = a + b + c + d + e;
865 let lo = min(min(min(ta, tb), min(tc, td)), te);
866 let hi = max(max(max(ta, tb), max(tc, td)), te);
867 return clamp(sum / weight, lo, hi);
868}
869
870fn warped(position: vec4f) -> Found {
871 let goal = position.xy * warp.size.zw;
872 var found = pick(goal);
873 let outside = any(found.uv < vec2f(0.0)) || any(found.uv > vec2f(1.0));
874 if (found.miss >= warp.limits.y || outside) {
875 atomicAdd(&holes[0], 1u);
876 }
877 found.uv = clamp(found.uv, vec2f(0.0), vec2f(1.0));
878 return found;
879}
880
881fn resample(uv: vec2f) -> vec4f {
882 let at = uv * warp.size.xy;
883 let centre = floor(at) + 0.5;
884 if (all(abs(at - centre) < vec2f(0.002))) {
885 return textureLoad(colour, vec2i(floor(at)), 0);
886 }
887 return catmull_rom(uv);
888}
889
890@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
891 let found = warped(position);
892 let base = resample(found.uv);
893 if (warp.limits.w > 0.5) {
894 let over = textureLoad(overlay, vec2i(position.xy), 0);
895 return over + base * (1.0 - over.a);
896 }
897 return base;
898}
899
900struct Layered {
901 @location(0) colour: vec4f,
902 @location(1) depth: f32,
903}
904
905@fragment fn layered(@builtin(position) position: vec4f) -> Layered {
906 let found = warped(position);
907 var out: Layered;
908 out.colour = resample(found.uv);
909 out.depth = found.depth;
910 return out;
911}
912"#;
913
914pub const WAKE_GROUPS: u32 = 1024;
915pub const TILE: u32 = 4;
916
917pub(crate) const TILES_WGSL: &str = r#"
918@group(0) @binding(0) var depth: texture_2d<f32>;
919@group(0) @binding(1) var raw: texture_storage_2d<rg32float, write>;
920@compute @workgroup_size(8, 8)
921fn reduce(@builtin(global_invocation_id) id: vec3u) {
922 let tiles = textureDimensions(raw);
923 if (id.x >= tiles.x || id.y >= tiles.y) {
924 return;
925 }
926 let size = vec2i(textureDimensions(depth));
927 let origin = vec2i(id.xy) * 4;
928 var lo = 3.0e38;
929 var hi = 0.0;
930 for (var y = 0; y < 4; y = y + 1) {
931 for (var x = 0; x < 4; x = x + 1) {
932 let d = textureLoad(depth, min(origin + vec2i(x, y), size - vec2i(1)), 0).r;
933 lo = min(lo, d);
934 hi = max(hi, d);
935 }
936 }
937 textureStore(raw, vec2i(id.xy), vec4f(lo, hi, 0.0, 0.0));
938}
939"#;
940
941pub(crate) const SPREAD_WGSL: &str = r#"
942@group(0) @binding(0) var raw: texture_2d<f32>;
943@group(0) @binding(1) var tiles: texture_storage_2d<rg32float, write>;
944@compute @workgroup_size(8, 8)
945fn spread(@builtin(global_invocation_id) id: vec3u) {
946 let size = vec2i(textureDimensions(raw));
947 if (i32(id.x) >= size.x || i32(id.y) >= size.y) {
948 return;
949 }
950 var lo = 3.0e38;
951 var hi = 0.0;
952 for (var y = -1; y <= 1; y = y + 1) {
953 for (var x = -1; x <= 1; x = x + 1) {
954 let p = clamp(vec2i(id.xy) + vec2i(x, y), vec2i(0), size - vec2i(1));
955 let range = textureLoad(raw, p, 0).rg;
956 lo = min(lo, range.x);
957 hi = max(hi, range.y);
958 }
959 }
960 textureStore(tiles, vec2i(id.xy), vec4f(lo, hi, 0.0, 0.0));
961}
962"#;
963
964pub(crate) const WAKE_WGSL: &str = r#"
965@group(0) @binding(0) var<storage, read_write> sink: array<f32, 4>;
966@compute @workgroup_size(64)
967fn main(@builtin(global_invocation_id) id: vec3u) {
968 var x = f32(id.x) * 1.0e-3;
969 for (var step = 0u; step < 2048u; step = step + 1u) {
970 x = fma(x, 1.0000001, 1.0e-7);
971 }
972 if (x == 12345.0) {
973 sink[id.x & 3u] = x;
974 }
975}
976"#;
977
978pub(crate) const COMPOSITE_WGSL: &str = r#"
979@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
980 let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
981 return vec4f(xy[index], 0.0, 1.0);
982}
983@group(0) @binding(0) var base: texture_2d<f32>;
984@group(0) @binding(1) var overlay: texture_2d<f32>;
985@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
986 let under = textureLoad(base, vec2i(position.xy), 0);
987 let over = textureLoad(overlay, vec2i(position.xy), 0);
988 return over + under * (1.0 - over.a);
989}
990"#;
991
992pub(crate) const COPY_WGSL: &str = r#"
993@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
994 let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
995 return vec4f(xy[index], 0.0, 1.0);
996}
997@group(0) @binding(0) var source: texture_2d<f32>;
998@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
999 return textureLoad(source, vec2i(position.xy), 0);
1000}
1001"#;
1002
1003#[derive(Clone, Copy)]
1004pub(crate) struct WarpUniform {
1005 pub across: [f32; 4],
1006 pub down: [f32; 4],
1007 pub corner: [f32; 4],
1008 pub origin: [f32; 4],
1009 pub size: [f32; 4],
1010 pub limits: [f32; 4],
1011}
1012
1013impl WarpUniform {
1014 pub fn new(
1015 from: (&Matrix, &Matrix),
1016 to: (&Matrix, &Matrix),
1017 size: [u32; 2],
1018 far: f32,
1019 ) -> Option<Self> {
1020 let (view0, projection0) = from;
1021 let (view1, projection1) = to;
1022 let inverse_projection = invert(*projection0)?;
1023 let mapped = multiply(*projection1, multiply(*view1, invert(*view0)?));
1024 let ray = |u: f32, v: f32| {
1025 let h = transform(
1026 &inverse_projection,
1027 [u * 2.0 - 1.0, 1.0 - v * 2.0, 0.5, 1.0],
1028 );
1029 [h[0] / -h[2], h[1] / -h[2]]
1030 };
1031 let start = ray(0.0, 0.0);
1032 let right = ray(1.0, 0.0)[0] - start[0];
1033 let below = ray(0.0, 1.0)[1] - start[1];
1034 let column = |index: usize| mapped[index];
1035 let scaled = |vector: [f32; 4], by: f32| vector.map(|value| value * by);
1036 let corner: [f32; 4] = std::array::from_fn(|row| {
1037 column(0)[row] * start[0] + column(1)[row] * start[1] - column(2)[row]
1038 });
1039 Some(Self {
1040 across: scaled(column(0), right),
1041 down: scaled(column(1), below),
1042 corner,
1043 origin: column(3),
1044 size: [
1045 size[0] as f32,
1046 size[1] as f32,
1047 1.0 / size[0] as f32,
1048 1.0 / size[1] as f32,
1049 ],
1050 limits: [far * 0.999, 0.75, TILE as f32, 0.0],
1051 })
1052 }
1053
1054 pub fn bytes(&self) -> Vec<u8> {
1055 let mut values: Vec<f32> = Vec::with_capacity(24);
1056 for vector in [
1057 self.across,
1058 self.down,
1059 self.corner,
1060 self.origin,
1061 self.size,
1062 self.limits,
1063 ] {
1064 values.extend(vector);
1065 }
1066 bytemuck::cast_slice(&values).to_vec()
1067 }
1068}
1069
1070pub(crate) struct Readback {
1071 buffer: wgpu::Buffer,
1072 pending: Option<Receiver<Result<(), wgpu::BufferAsyncError>>>,
1073 encoded: bool,
1074}
1075
1076impl Readback {
1077 fn new(device: &wgpu::Device, size: u64, label: &str) -> Self {
1078 Self {
1079 buffer: device.create_buffer(&wgpu::BufferDescriptor {
1080 label: Some(label),
1081 size,
1082 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1083 mapped_at_creation: false,
1084 }),
1085 pending: None,
1086 encoded: false,
1087 }
1088 }
1089
1090 pub fn free(&self) -> bool {
1091 self.pending.is_none() && !self.encoded
1092 }
1093
1094 pub fn submitted(&mut self) {
1095 if !self.encoded {
1096 return;
1097 }
1098 self.encoded = false;
1099 let (sender, receiver) = std::sync::mpsc::channel();
1100 self.buffer
1101 .slice(..)
1102 .map_async(wgpu::MapMode::Read, move |result| {
1103 let _ = sender.send(result);
1104 });
1105 self.pending = Some(receiver);
1106 }
1107
1108 pub fn take(&mut self) -> Option<Vec<u8>> {
1109 let receiver = self.pending.as_ref()?;
1110 match receiver.try_recv() {
1111 Ok(Ok(())) => {
1112 let bytes = self
1113 .buffer
1114 .slice(..)
1115 .get_mapped_range()
1116 .expect("a mapped range")
1117 .to_vec();
1118 self.buffer.unmap();
1119 self.pending = None;
1120 Some(bytes)
1121 }
1122 Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
1123 self.pending = None;
1124 None
1125 }
1126 Err(TryRecvError::Empty) => None,
1127 }
1128 }
1129}
1130
1131pub(crate) struct Image {
1132 pub view: wgpu::TextureView,
1133}
1134
1135fn image(
1136 device: &wgpu::Device,
1137 size: [u32; 2],
1138 format: wgpu::TextureFormat,
1139 usage: wgpu::TextureUsages,
1140 label: &str,
1141) -> Image {
1142 let texture = device.create_texture(&wgpu::TextureDescriptor {
1143 label: Some(label),
1144 size: wgpu::Extent3d {
1145 width: size[0],
1146 height: size[1],
1147 depth_or_array_layers: 1,
1148 },
1149 mip_level_count: 1,
1150 sample_count: 1,
1151 dimension: wgpu::TextureDimension::D2,
1152 format,
1153 usage,
1154 view_formats: &[],
1155 });
1156 Image {
1157 view: texture.create_view(&Default::default()),
1158 }
1159}
1160
1161#[derive(Clone, Copy)]
1162pub(crate) enum Last {
1163 Colour,
1164 Scratch,
1165 Warp(WarpUniform),
1166}
1167
1168pub(crate) struct Cached {
1169 pub view: Matrix,
1170 pub projection: Matrix,
1171 pub far: f32,
1172 pub depth_index: usize,
1173 pub shadow: Vec<u8>,
1174 pub frame_number: u32,
1175 pub exposure: f32,
1176}
1177
1178pub(crate) struct Cache {
1179 pub size: [u32; 2],
1180 pub colour: Image,
1181 pub still: Image,
1182 pub layer: Image,
1183 pub depth: Image,
1184 pub scratch: Image,
1185 pub tiles: Image,
1186 raw_tiles: Image,
1187 pub limits: Limits,
1188 pub book: Book,
1189 pub cached: Option<Cached>,
1190 pub exposure_moving: bool,
1191 pub colour_layers: bool,
1192 pub last: Last,
1193 pub overlaid: bool,
1194 pub overlay_print: Option<u64>,
1195 pub overlay: Image,
1196 layout: wgpu::BindGroupLayout,
1197 module: wgpu::ShaderModule,
1198 pipeline_layout: wgpu::PipelineLayout,
1199 pipelines: Vec<(wgpu::TextureFormat, bool, wgpu::RenderPipeline)>,
1200 copy_layout: wgpu::BindGroupLayout,
1201 copy_pipeline: wgpu::RenderPipeline,
1202 sampler: wgpu::Sampler,
1203 holes: wgpu::Buffer,
1204 wake: Option<(wgpu::ComputePipeline, wgpu::BindGroup)>,
1205 tiler: wgpu::ComputePipeline,
1206 spreader: wgpu::ComputePipeline,
1207 composite_layout: wgpu::BindGroupLayout,
1208 composite_pipeline_layout: wgpu::PipelineLayout,
1209 composite_module: wgpu::ShaderModule,
1210 composites: Vec<(wgpu::TextureFormat, wgpu::RenderPipeline)>,
1211 holes_back: Readback,
1212 depth_back: Readback,
1213}
1214
1215const COLOUR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
1216
1217impl Cache {
1218 pub fn new(device: &wgpu::Device, size: [u32; 2], limits: Limits) -> Self {
1219 let colour = image(
1220 device,
1221 size,
1222 COLOUR,
1223 wgpu::TextureUsages::RENDER_ATTACHMENT
1224 | wgpu::TextureUsages::TEXTURE_BINDING
1225 | wgpu::TextureUsages::STORAGE_BINDING,
1226 "demand colour",
1227 );
1228 let still = image(
1229 device,
1230 size,
1231 COLOUR,
1232 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1233 "demand still",
1234 );
1235 let layer = image(
1236 device,
1237 size,
1238 COLOUR,
1239 wgpu::TextureUsages::RENDER_ATTACHMENT
1240 | wgpu::TextureUsages::TEXTURE_BINDING
1241 | wgpu::TextureUsages::STORAGE_BINDING,
1242 "demand layer",
1243 );
1244 let depth = image(
1245 device,
1246 size,
1247 wgpu::TextureFormat::R32Float,
1248 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1249 "demand warped depth",
1250 );
1251 let scratch = image(
1252 device,
1253 size,
1254 COLOUR,
1255 wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
1256 "demand post",
1257 );
1258 let tiles = image(
1259 device,
1260 [size[0].div_ceil(TILE), size[1].div_ceil(TILE)],
1261 wgpu::TextureFormat::Rg32Float,
1262 wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
1263 "demand depth tiles",
1264 );
1265 let raw_tiles = image(
1266 device,
1267 [size[0].div_ceil(TILE), size[1].div_ceil(TILE)],
1268 wgpu::TextureFormat::Rg32Float,
1269 wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
1270 "demand raw depth tiles",
1271 );
1272 let compute = |source: &str, entry: &str, label: &str| {
1273 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1274 label: Some(label),
1275 source: wgpu::ShaderSource::Wgsl(source.into()),
1276 });
1277 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1278 label: Some(label),
1279 layout: None,
1280 module: &module,
1281 entry_point: Some(entry),
1282 compilation_options: Default::default(),
1283 cache: None,
1284 })
1285 };
1286 let tiler = compute(TILES_WGSL, "reduce", "demand depth tiles");
1287 let spreader = compute(SPREAD_WGSL, "spread", "demand depth spread");
1288 let float = |binding: u32, filterable: bool| wgpu::BindGroupLayoutEntry {
1289 binding,
1290 visibility: wgpu::ShaderStages::FRAGMENT,
1291 ty: wgpu::BindingType::Texture {
1292 sample_type: wgpu::TextureSampleType::Float { filterable },
1293 view_dimension: wgpu::TextureViewDimension::D2,
1294 multisampled: false,
1295 },
1296 count: None,
1297 };
1298 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1299 label: Some("demand warp"),
1300 entries: &[
1301 wgpu::BindGroupLayoutEntry {
1302 binding: 0,
1303 visibility: wgpu::ShaderStages::FRAGMENT,
1304 ty: wgpu::BindingType::Buffer {
1305 ty: wgpu::BufferBindingType::Uniform,
1306 has_dynamic_offset: false,
1307 min_binding_size: None,
1308 },
1309 count: None,
1310 },
1311 float(1, true),
1312 float(2, false),
1313 wgpu::BindGroupLayoutEntry {
1314 binding: 3,
1315 visibility: wgpu::ShaderStages::FRAGMENT,
1316 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1317 count: None,
1318 },
1319 wgpu::BindGroupLayoutEntry {
1320 binding: 4,
1321 visibility: wgpu::ShaderStages::FRAGMENT,
1322 ty: wgpu::BindingType::Buffer {
1323 ty: wgpu::BufferBindingType::Storage { read_only: false },
1324 has_dynamic_offset: false,
1325 min_binding_size: None,
1326 },
1327 count: None,
1328 },
1329 float(5, false),
1330 float(6, false),
1331 ],
1332 });
1333 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1334 label: Some("demand warp"),
1335 bind_group_layouts: &[Some(&layout)],
1336 immediate_size: 0,
1337 });
1338 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1339 label: Some("demand warp"),
1340 source: wgpu::ShaderSource::Wgsl(WARP_WGSL.into()),
1341 });
1342 let overlay = image(
1343 device,
1344 size,
1345 COLOUR,
1346 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1347 "demand overlay",
1348 );
1349 let composite_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1350 label: Some("demand composite"),
1351 entries: &[float(0, false), float(1, false)],
1352 });
1353 let composite_pipeline_layout =
1354 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1355 label: Some("demand composite"),
1356 bind_group_layouts: &[Some(&composite_layout)],
1357 immediate_size: 0,
1358 });
1359 let composite_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1360 label: Some("demand composite"),
1361 source: wgpu::ShaderSource::Wgsl(COMPOSITE_WGSL.into()),
1362 });
1363 let copy_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1364 label: Some("demand copy"),
1365 entries: &[float(0, false)],
1366 });
1367 let copy_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1368 label: Some("demand copy"),
1369 bind_group_layouts: &[Some(©_layout)],
1370 immediate_size: 0,
1371 });
1372 let copy_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1373 label: Some("demand copy"),
1374 source: wgpu::ShaderSource::Wgsl(COPY_WGSL.into()),
1375 });
1376 let copy_pipeline = pipeline(
1377 device,
1378 ©_pipeline_layout,
1379 ©_module,
1380 "fragment",
1381 &[COLOUR],
1382 "demand copy",
1383 );
1384 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1385 label: Some("demand warp"),
1386 mag_filter: wgpu::FilterMode::Linear,
1387 min_filter: wgpu::FilterMode::Linear,
1388 address_mode_u: wgpu::AddressMode::ClampToEdge,
1389 address_mode_v: wgpu::AddressMode::ClampToEdge,
1390 ..Default::default()
1391 });
1392 let holes = device.create_buffer(&wgpu::BufferDescriptor {
1393 label: Some("demand holes"),
1394 size: 16,
1395 usage: wgpu::BufferUsages::STORAGE
1396 | wgpu::BufferUsages::COPY_SRC
1397 | wgpu::BufferUsages::COPY_DST,
1398 mapped_at_creation: false,
1399 });
1400 Self {
1401 size,
1402 colour,
1403 still,
1404 layer,
1405 depth,
1406 scratch,
1407 tiles,
1408 raw_tiles,
1409 limits,
1410 book: Book::default(),
1411 cached: None,
1412 exposure_moving: false,
1413 colour_layers: false,
1414 last: Last::Colour,
1415 overlaid: false,
1416 overlay_print: None,
1417 overlay,
1418 layout,
1419 module,
1420 pipeline_layout,
1421 pipelines: Vec::new(),
1422 copy_layout,
1423 copy_pipeline,
1424 sampler,
1425 holes,
1426 wake: None,
1427 tiler,
1428 spreader,
1429 composite_layout,
1430 composite_pipeline_layout,
1431 composite_module,
1432 composites: Vec::new(),
1433 holes_back: Readback::new(device, 16, "demand holes readback"),
1434 depth_back: Readback::new(device, DEPTH_PROBES as u64 * 256, "demand depth readback"),
1435 }
1436 }
1437
1438 pub fn stats(&self) -> DemandStats {
1439 DemandStats {
1440 shown: self.book.kind,
1441 since_full: self.book.since_full,
1442 settle: self.book.settle,
1443 holes: self.book.holes,
1444 depth: self.book.depth,
1445 }
1446 }
1447
1448 pub fn gather(&mut self) {
1449 if let Some(bytes) = self.holes_back.take() {
1450 let count = u32::from_le_bytes(bytes[..4].try_into().unwrap());
1451 let pixels = (u64::from(self.size[0]) * u64::from(self.size[1])).max(1);
1452 self.book.holes = count as f32 / pixels as f32;
1453 }
1454 if let Some(bytes) = self.depth_back.take() {
1455 let far = self
1456 .cached
1457 .as_ref()
1458 .map_or(f32::INFINITY, |cached| cached.far);
1459 let mut depths: Vec<f32> = (0..DEPTH_PROBES)
1460 .map(|index| {
1461 f32::from_le_bytes(bytes[index * 256..index * 256 + 4].try_into().unwrap())
1462 })
1463 .filter(|depth| depth.is_finite() && *depth > 0.0 && *depth < far * 0.999)
1464 .collect();
1465 depths.sort_by(f32::total_cmp);
1466 self.book.depth = Some(
1467 depths
1468 .get(depths.len() / 2)
1469 .copied()
1470 .unwrap_or(f32::INFINITY),
1471 );
1472 }
1473 }
1474
1475 pub fn submitted(&mut self) {
1476 self.holes_back.submitted();
1477 self.depth_back.submitted();
1478 }
1479
1480 pub fn probe_depth(&mut self, encoder: &mut wgpu::CommandEncoder, depth: &wgpu::Texture) {
1481 if !self.depth_back.free() {
1482 return;
1483 }
1484 let [width, height] = self.size;
1485 for index in 0..DEPTH_PROBES {
1486 let column = (index % 4) as u32;
1487 let row = (index / 4) as u32;
1488 let x = ((2 * column + 1) * width / 8).min(width - 1);
1489 let y = ((2 * row + 1) * height / 8).min(height - 1);
1490 encoder.copy_texture_to_buffer(
1491 wgpu::TexelCopyTextureInfo {
1492 texture: depth,
1493 mip_level: 0,
1494 origin: wgpu::Origin3d { x, y, z: 0 },
1495 aspect: wgpu::TextureAspect::All,
1496 },
1497 wgpu::TexelCopyBufferInfo {
1498 buffer: &self.depth_back.buffer,
1499 layout: wgpu::TexelCopyBufferLayout {
1500 offset: index as u64 * 256,
1501 bytes_per_row: Some(256),
1502 rows_per_image: Some(1),
1503 },
1504 },
1505 wgpu::Extent3d {
1506 width: 1,
1507 height: 1,
1508 depth_or_array_layers: 1,
1509 },
1510 );
1511 }
1512 self.depth_back.encoded = true;
1513 }
1514
1515 pub fn encode_clear(&self, encoder: &mut wgpu::CommandEncoder) {
1516 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1517 multiview_mask: None,
1518 label: Some("demand overlay clear"),
1519 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1520 view: &self.overlay.view,
1521 resolve_target: None,
1522 depth_slice: None,
1523 ops: wgpu::Operations {
1524 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1525 store: wgpu::StoreOp::Store,
1526 },
1527 })],
1528 depth_stencil_attachment: None,
1529 timestamp_writes: None,
1530 occlusion_query_set: None,
1531 });
1532 }
1533
1534 pub fn encode_composite(
1535 &mut self,
1536 device: &wgpu::Device,
1537 encoder: &mut wgpu::CommandEncoder,
1538 base: &wgpu::TextureView,
1539 target: (&wgpu::TextureView, wgpu::TextureFormat),
1540 profiler: &mut GpuProfiler,
1541 ) {
1542 let index = match self
1543 .composites
1544 .iter()
1545 .position(|(format, _)| *format == target.1)
1546 {
1547 Some(index) => index,
1548 None => {
1549 let built = pipeline(
1550 device,
1551 &self.composite_pipeline_layout,
1552 &self.composite_module,
1553 "fragment",
1554 &[target.1],
1555 "demand composite",
1556 );
1557 self.composites.push((target.1, built));
1558 self.composites.len() - 1
1559 }
1560 };
1561 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1562 label: Some("demand composite"),
1563 layout: &self.composite_layout,
1564 entries: &[
1565 wgpu::BindGroupEntry {
1566 binding: 0,
1567 resource: wgpu::BindingResource::TextureView(base),
1568 },
1569 wgpu::BindGroupEntry {
1570 binding: 1,
1571 resource: wgpu::BindingResource::TextureView(&self.overlay.view),
1572 },
1573 ],
1574 });
1575 let timing = profiler.pass("composite");
1576 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1577 multiview_mask: None,
1578 label: Some("demand composite"),
1579 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1580 view: target.0,
1581 resolve_target: None,
1582 depth_slice: None,
1583 ops: wgpu::Operations {
1584 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1585 store: wgpu::StoreOp::Store,
1586 },
1587 })],
1588 depth_stencil_attachment: None,
1589 timestamp_writes: profiler.render_writes(timing),
1590 occlusion_query_set: None,
1591 });
1592 pass.set_pipeline(&self.composites[index].1);
1593 pass.set_bind_group(0, &bind, &[]);
1594 pass.draw(0..3, 0..1);
1595 }
1596
1597 pub fn encode_tiles(
1598 &self,
1599 device: &wgpu::Device,
1600 encoder: &mut wgpu::CommandEncoder,
1601 depth: &wgpu::TextureView,
1602 profiler: &mut GpuProfiler,
1603 ) {
1604 let [width, height] = [self.size[0].div_ceil(TILE), self.size[1].div_ceil(TILE)];
1605 let timing = profiler.pass("demand tiles");
1606 let writes = profiler.compute_writes(timing);
1607 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1608 label: Some("demand depth tiles"),
1609 timestamp_writes: writes,
1610 });
1611 for (pipeline, source, target) in [
1612 (&self.tiler, depth, &self.raw_tiles.view),
1613 (&self.spreader, &self.raw_tiles.view, &self.tiles.view),
1614 ] {
1615 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1616 label: Some("demand depth tiles"),
1617 layout: &pipeline.get_bind_group_layout(0),
1618 entries: &[
1619 wgpu::BindGroupEntry {
1620 binding: 0,
1621 resource: wgpu::BindingResource::TextureView(source),
1622 },
1623 wgpu::BindGroupEntry {
1624 binding: 1,
1625 resource: wgpu::BindingResource::TextureView(target),
1626 },
1627 ],
1628 });
1629 pass.set_pipeline(pipeline);
1630 pass.set_bind_group(0, &bind, &[]);
1631 pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
1632 }
1633 }
1634
1635 pub fn encode_wake(
1636 &mut self,
1637 device: &wgpu::Device,
1638 encoder: &mut wgpu::CommandEncoder,
1639 groups: u32,
1640 profiler: &mut GpuProfiler,
1641 ) {
1642 let (pipeline, bind) = self.wake.get_or_insert_with(|| {
1643 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1644 label: Some("demand wake"),
1645 source: wgpu::ShaderSource::Wgsl(WAKE_WGSL.into()),
1646 });
1647 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1648 label: Some("demand wake"),
1649 layout: None,
1650 module: &module,
1651 entry_point: Some("main"),
1652 compilation_options: Default::default(),
1653 cache: None,
1654 });
1655 let sink = device.create_buffer(&wgpu::BufferDescriptor {
1656 label: Some("demand wake"),
1657 size: 16,
1658 usage: wgpu::BufferUsages::STORAGE,
1659 mapped_at_creation: false,
1660 });
1661 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1662 label: Some("demand wake"),
1663 layout: &pipeline.get_bind_group_layout(0),
1664 entries: &[wgpu::BindGroupEntry {
1665 binding: 0,
1666 resource: sink.as_entire_binding(),
1667 }],
1668 });
1669 (pipeline, bind)
1670 });
1671 let timing = profiler.pass("wake");
1672 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1673 label: Some("demand wake"),
1674 timestamp_writes: profiler.compute_writes(timing),
1675 });
1676 pass.set_pipeline(pipeline);
1677 pass.set_bind_group(0, &*bind, &[]);
1678 pass.dispatch_workgroups(groups.max(1), 1, 1);
1679 }
1680
1681 fn warp_pipeline(
1682 &mut self,
1683 device: &wgpu::Device,
1684 format: wgpu::TextureFormat,
1685 layered: bool,
1686 ) -> usize {
1687 if let Some(index) = self
1688 .pipelines
1689 .iter()
1690 .position(|(known, kind, _)| *known == format && *kind == layered)
1691 {
1692 return index;
1693 }
1694 let built = if layered {
1695 pipeline(
1696 device,
1697 &self.pipeline_layout,
1698 &self.module,
1699 "layered",
1700 &[format, wgpu::TextureFormat::R32Float],
1701 "demand warp layered",
1702 )
1703 } else {
1704 pipeline(
1705 device,
1706 &self.pipeline_layout,
1707 &self.module,
1708 "fragment",
1709 &[format],
1710 "demand warp",
1711 )
1712 };
1713 self.pipelines.push((format, layered, built));
1714 self.pipelines.len() - 1
1715 }
1716
1717 #[allow(clippy::too_many_arguments)]
1718 pub fn encode_warp(
1719 &mut self,
1720 device: &wgpu::Device,
1721 encoder: &mut wgpu::CommandEncoder,
1722 uniform: &WarpUniform,
1723 source: &wgpu::TextureView,
1724 depth: &wgpu::TextureView,
1725 target: (&wgpu::TextureView, wgpu::TextureFormat),
1726 layered: bool,
1727 profiler: &mut GpuProfiler,
1728 ) {
1729 let index = self.warp_pipeline(device, target.1, layered);
1730 let buffer = wgpu::util::DeviceExt::create_buffer_init(
1731 device,
1732 &wgpu::util::BufferInitDescriptor {
1733 label: Some("demand warp"),
1734 contents: &uniform.bytes(),
1735 usage: wgpu::BufferUsages::UNIFORM,
1736 },
1737 );
1738 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1739 label: Some("demand warp"),
1740 layout: &self.layout,
1741 entries: &[
1742 wgpu::BindGroupEntry {
1743 binding: 0,
1744 resource: buffer.as_entire_binding(),
1745 },
1746 wgpu::BindGroupEntry {
1747 binding: 1,
1748 resource: wgpu::BindingResource::TextureView(source),
1749 },
1750 wgpu::BindGroupEntry {
1751 binding: 2,
1752 resource: wgpu::BindingResource::TextureView(depth),
1753 },
1754 wgpu::BindGroupEntry {
1755 binding: 3,
1756 resource: wgpu::BindingResource::Sampler(&self.sampler),
1757 },
1758 wgpu::BindGroupEntry {
1759 binding: 4,
1760 resource: self.holes.as_entire_binding(),
1761 },
1762 wgpu::BindGroupEntry {
1763 binding: 5,
1764 resource: wgpu::BindingResource::TextureView(&self.tiles.view),
1765 },
1766 wgpu::BindGroupEntry {
1767 binding: 6,
1768 resource: wgpu::BindingResource::TextureView(&self.overlay.view),
1769 },
1770 ],
1771 });
1772 let measure = self.holes_back.free();
1773 if measure {
1774 encoder.clear_buffer(&self.holes, 0, None);
1775 }
1776 let timing = profiler.pass("reproject");
1777 {
1778 let mut attachments = vec![Some(wgpu::RenderPassColorAttachment {
1779 view: target.0,
1780 resolve_target: None,
1781 depth_slice: None,
1782 ops: wgpu::Operations {
1783 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1784 store: wgpu::StoreOp::Store,
1785 },
1786 })];
1787 if layered {
1788 attachments.push(Some(wgpu::RenderPassColorAttachment {
1789 view: &self.depth.view,
1790 resolve_target: None,
1791 depth_slice: None,
1792 ops: wgpu::Operations {
1793 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1794 store: wgpu::StoreOp::Store,
1795 },
1796 }));
1797 }
1798 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1799 multiview_mask: None,
1800 label: Some("demand reproject"),
1801 color_attachments: &attachments,
1802 depth_stencil_attachment: None,
1803 timestamp_writes: profiler.render_writes(timing),
1804 occlusion_query_set: None,
1805 });
1806 pass.set_pipeline(&self.pipelines[index].2);
1807 pass.set_bind_group(0, &bind, &[]);
1808 pass.draw(0..3, 0..1);
1809 }
1810 if measure {
1811 encoder.copy_buffer_to_buffer(&self.holes, 0, &self.holes_back.buffer, 0, 16);
1812 self.holes_back.encoded = true;
1813 }
1814 }
1815
1816 #[allow(clippy::too_many_arguments)]
1817 pub fn encode_copy(
1818 &self,
1819 device: &wgpu::Device,
1820 encoder: &mut wgpu::CommandEncoder,
1821 source: &wgpu::TextureView,
1822 target: &wgpu::TextureView,
1823 regions: &[Region],
1824 label: &'static str,
1825 profiler: &mut GpuProfiler,
1826 ) {
1827 if regions.is_empty() {
1828 return;
1829 }
1830 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
1831 label: Some("demand copy"),
1832 layout: &self.copy_layout,
1833 entries: &[wgpu::BindGroupEntry {
1834 binding: 0,
1835 resource: wgpu::BindingResource::TextureView(source),
1836 }],
1837 });
1838 let timing = profiler.pass(label);
1839 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1840 multiview_mask: None,
1841 label: Some(label),
1842 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1843 view: target,
1844 resolve_target: None,
1845 depth_slice: None,
1846 ops: wgpu::Operations {
1847 load: wgpu::LoadOp::Load,
1848 store: wgpu::StoreOp::Store,
1849 },
1850 })],
1851 depth_stencil_attachment: None,
1852 timestamp_writes: profiler.render_writes(timing),
1853 occlusion_query_set: None,
1854 });
1855 pass.set_pipeline(&self.copy_pipeline);
1856 pass.set_bind_group(0, &bind, &[]);
1857 for region in regions {
1858 let region = region.clip(self.size[0], self.size[1]);
1859 if region.is_empty() {
1860 continue;
1861 }
1862 pass.set_scissor_rect(region.x, region.y, region.width, region.height);
1863 pass.draw(0..3, 0..1);
1864 }
1865 }
1866}
1867
1868fn pipeline(
1869 device: &wgpu::Device,
1870 layout: &wgpu::PipelineLayout,
1871 module: &wgpu::ShaderModule,
1872 entry: &str,
1873 formats: &[wgpu::TextureFormat],
1874 label: &str,
1875) -> wgpu::RenderPipeline {
1876 let targets: Vec<Option<wgpu::ColorTargetState>> = formats
1877 .iter()
1878 .map(|format| {
1879 Some(wgpu::ColorTargetState {
1880 format: *format,
1881 blend: None,
1882 write_mask: wgpu::ColorWrites::ALL,
1883 })
1884 })
1885 .collect();
1886 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1887 label: Some(label),
1888 layout: Some(layout),
1889 vertex: wgpu::VertexState {
1890 module,
1891 entry_point: Some("vertex"),
1892 buffers: &[],
1893 compilation_options: Default::default(),
1894 },
1895 primitive: Default::default(),
1896 depth_stencil: None,
1897 multisample: Default::default(),
1898 fragment: Some(wgpu::FragmentState {
1899 module,
1900 entry_point: Some(entry),
1901 targets: &targets,
1902 compilation_options: Default::default(),
1903 }),
1904 multiview_mask: None,
1905 cache: None,
1906 })
1907}
1908
1909#[cfg(test)]
1910mod tests {
1911 use super::*;
1912
1913 const W: u32 = 1280;
1914 const H: u32 = 800;
1915
1916 fn projection(scale: f32) -> Matrix {
1917 let near = 0.05;
1918 let far = 40.0;
1919 [
1920 [scale / 1.6, 0.0, 0.0, 0.0],
1921 [0.0, scale, 0.0, 0.0],
1922 [0.0, 0.0, far / (near - far), -1.0],
1923 [0.0, 0.0, far * near / (near - far), 0.0],
1924 ]
1925 }
1926
1927 fn view(yaw: f32, position: [f32; 3]) -> Matrix {
1928 let (s, c) = yaw.sin_cos();
1929 let right = [c, 0.0, -s];
1930 let up = [0.0, 1.0, 0.0];
1931 let back = [s, 0.0, c];
1932 let dot = |a: [f32; 3]| a[0] * position[0] + a[1] * position[1] + a[2] * position[2];
1933 [
1934 [right[0], up[0], back[0], 0.0],
1935 [right[1], up[1], back[1], 0.0],
1936 [right[2], up[2], back[2], 0.0],
1937 [-dot(right), -dot(up), -dot(back), 1.0],
1938 ]
1939 }
1940
1941 fn summary() -> Summary {
1942 Summary {
1943 view: view(0.0, [0.0, 1.0, 2.0]),
1944 projection: projection(1.5),
1945 position: [0.0, 1.0, 2.0],
1946 size: [W, H],
1947 world: 7,
1948 anchors: Vec::new(),
1949 overlay: 3,
1950 ticking: false,
1951 layered: Vec::new(),
1952 moving: Vec::new(),
1953 }
1954 }
1955
1956 fn turned(degrees: f32) -> Summary {
1957 Summary {
1958 view: view(degrees.to_radians(), [0.0, 1.0, 2.0]),
1959 ..summary()
1960 }
1961 }
1962
1963 fn settled(shown: Summary) -> Book {
1964 let limits = Limits {
1965 settle: 0,
1966 ..Limits::default()
1967 };
1968 let mut book = Book::default();
1969 record(&mut book, shown, Shown::Full, &limits);
1970 book.depth = Some(2.0);
1971 book.still = true;
1972 book
1973 }
1974
1975 #[test]
1976 fn regions_clip_grow_and_merge() {
1977 let region = Region::new(10, 20, 30, 40);
1978 assert_eq!(region.grow(15, W, H), Region::new(0, 5, 55, 70));
1979 assert_eq!(
1980 Region::new(1270, 790, 50, 50).clip(W, H),
1981 Region::new(1270, 790, 10, 10)
1982 );
1983 assert!(Region::new(1300, 10, 5, 5).clip(W, H).is_empty());
1984 assert_eq!(
1985 Region::from_bounds([10.2, 20.9, 40.1, 60.0], W, H),
1986 Some(Region::new(10, 20, 31, 40))
1987 );
1988 assert_eq!(Region::from_bounds([50.0, 50.0, 40.0, 60.0], W, H), None);
1989 assert_eq!(
1990 Region::from_bounds([f32::NAN, 0.0, 1.0, 1.0], W, H),
1991 Some(Region::whole(W, H))
1992 );
1993 let merged = merge(
1994 &[
1995 Region::new(0, 0, 10, 10),
1996 Region::new(20, 0, 10, 10),
1997 Region::new(600, 400, 10, 10),
1998 ],
1999 W,
2000 H,
2001 );
2002 assert_eq!(
2003 merged,
2004 vec![Region::new(0, 0, 30, 10), Region::new(600, 400, 10, 10)]
2005 );
2006 assert_eq!(
2007 merge(&[Region::new(0, 0, 1000, 700)], W, H),
2008 vec![Region::whole(W, H)]
2009 );
2010 assert!(merge(&[Region::new(0, 0, 0, 10)], W, H).is_empty());
2011 }
2012
2013 #[test]
2014 fn motion_measures_turns_shifts_and_zoom() {
2015 let base = summary();
2016 fn at(s: &Summary) -> (&Matrix, &Matrix, [f32; 3]) {
2017 (&s.view, &s.projection, s.position)
2018 }
2019 let still = motion(at(&base), at(&base), None).unwrap();
2020 assert!(still.angle.abs() < 1e-6 && still.zoom.abs() < 1e-6);
2021 let yaw = turned(0.25);
2022 let moved = motion(at(&base), at(&yaw), None).unwrap();
2023 assert!((moved.angle.to_degrees() - 0.25).abs() < 2e-3, "{moved:?}");
2024 let tiny = turned(0.002);
2025 let small = motion(at(&base), at(&tiny), None).unwrap();
2026 assert!((small.angle.to_degrees() - 0.002).abs() < 5e-4, "{small:?}");
2027 let pushed = Summary {
2028 view: view(0.0, [0.0, 1.0, 1.98]),
2029 position: [0.0, 1.0, 1.98],
2030 ..summary()
2031 };
2032 assert_eq!(motion(at(&base), at(&pushed), None), None);
2033 let shift = motion(at(&base), at(&pushed), Some(2.0)).unwrap();
2034 assert!((shift.angle - 0.01).abs() < 1e-4, "{shift:?}");
2035 let sky = motion(at(&base), at(&pushed), Some(f32::INFINITY)).unwrap();
2036 assert!(sky.angle < 1e-4);
2037 let zoomed = Summary {
2038 projection: projection(1.53),
2039 ..summary()
2040 };
2041 let zoom = motion(at(&base), at(&zoomed), None).unwrap();
2042 assert!((zoom.zoom - (1.53_f32 / 1.5).ln()).abs() < 1e-5);
2043 let mut squeezed = summary();
2044 squeezed.projection[0][0] *= 1.1;
2045 assert_eq!(motion(at(&base), at(&squeezed), None), None);
2046 let mut nearer = summary();
2047 nearer.projection[3][2] *= 0.5;
2048 assert_eq!(motion(at(&base), at(&nearer), None), None);
2049 }
2050
2051 #[test]
2052 fn nothing_cached_needs_a_full_frame() {
2053 assert_eq!(
2054 decide(&Book::default(), &summary(), &Limits::default()),
2055 Need::Full
2056 );
2057 }
2058
2059 #[test]
2060 fn an_unchanged_frame_needs_nothing() {
2061 let book = settled(summary());
2062 assert_eq!(decide(&book, &summary(), &Limits::default()), Need::None);
2063 }
2064
2065 #[test]
2066 fn each_kind_of_change_is_seen() {
2067 let limits = Limits::default();
2068 let book = settled(summary());
2069 let world = Summary {
2070 world: 8,
2071 ..summary()
2072 };
2073 assert_eq!(decide(&book, &world, &limits), Need::Full);
2074 let ticking = Summary {
2075 ticking: true,
2076 ..summary()
2077 };
2078 assert_eq!(decide(&book, &ticking, &limits), Need::Full);
2079 let resized = Summary {
2080 size: [W, H + 2],
2081 ..summary()
2082 };
2083 assert_eq!(decide(&book, &resized, &limits), Need::Full);
2084 let mut anchored = settled(Summary {
2085 anchors: vec![view(0.0, [0.0, 0.0, 0.0])],
2086 ..summary()
2087 });
2088 anchored.depth = Some(2.0);
2089 let nudged = Summary {
2090 anchors: vec![view(0.0, [0.0, 0.0, 1e-6])],
2091 ..summary()
2092 };
2093 assert_eq!(decide(&anchored, &nudged, &limits), Need::None);
2094 let slid = Summary {
2095 anchors: vec![view(0.0, [0.0, 0.01, 0.0])],
2096 ..summary()
2097 };
2098 assert_eq!(decide(&anchored, &slid, &limits), Need::Full);
2099 let overlay = Summary {
2100 overlay: 4,
2101 ..summary()
2102 };
2103 assert_eq!(
2104 decide(&book, &overlay, &limits),
2105 Need::Partial(vec![Region::whole(W, H)])
2106 );
2107 let moving = Summary {
2108 moving: vec![Region::new(10, 10, 40, 20)],
2109 ..summary()
2110 };
2111 assert_eq!(
2112 decide(&book, &moving, &limits),
2113 Need::Partial(vec![Region::new(10, 10, 40, 20)])
2114 );
2115 }
2116
2117 #[test]
2118 fn small_moves_reproject_and_large_ones_redraw() {
2119 let limits = Limits::default();
2120 let book = settled(summary());
2121 assert_eq!(decide(&book, &turned(0.3), &limits), Need::Reproject);
2122 assert_eq!(decide(&book, &turned(0.7), &limits), Need::Full);
2123 let zoomed = Summary {
2124 projection: projection(1.5 * 1.01),
2125 ..summary()
2126 };
2127 assert_eq!(decide(&book, &zoomed, &limits), Need::Reproject);
2128 let far_zoom = Summary {
2129 projection: projection(1.5 * 1.05),
2130 ..summary()
2131 };
2132 assert_eq!(decide(&book, &far_zoom, &limits), Need::Full);
2133 let mut unknown = book.clone();
2134 unknown.depth = None;
2135 let pushed = Summary {
2136 view: view(0.0, [0.0, 1.0, 1.995]),
2137 position: [0.0, 1.0, 1.995],
2138 ..summary()
2139 };
2140 assert_eq!(decide(&unknown, &pushed, &limits), Need::Full);
2141 assert_eq!(decide(&book, &pushed, &limits), Need::Reproject);
2142 let mut worn = book.clone();
2143 worn.since_full = limits.frames;
2144 assert_eq!(decide(&worn, &turned(0.1), &limits), Need::Full);
2145 let mut holed = book.clone();
2146 holed.holes = limits.holes * 2.0;
2147 assert_eq!(decide(&holed, &turned(0.1), &limits), Need::Full);
2148 }
2149
2150 #[test]
2151 fn a_camera_at_rest_after_reprojecting_gets_a_fresh_frame() {
2152 let limits = Limits::default();
2153 let mut book = settled(summary());
2154 record(&mut book, turned(0.2), Shown::Reproject, &limits);
2155 assert_eq!(book.since_full, 1);
2156 assert_eq!(decide(&book, &turned(0.25), &limits), Need::Reproject);
2157 assert_eq!(decide(&book, &turned(0.2), &limits), Need::Full);
2158 assert_eq!(decide(&book, &summary(), &limits), Need::Reproject);
2159 let steaming = Summary {
2160 layered: vec![Region::new(100, 100, 50, 80)],
2161 ..turned(0.2)
2162 };
2163 assert_eq!(decide(&book, &steaming, &limits), Need::Reproject);
2164 }
2165
2166 #[test]
2167 fn time_driven_layers_redraw_their_rects_and_where_they_were() {
2168 let limits = Limits::default();
2169 let steam = Region::new(100, 100, 50, 80);
2170 let mut book = settled(Summary {
2171 layered: vec![steam],
2172 ..summary()
2173 });
2174 let still = Summary {
2175 layered: vec![steam],
2176 ..summary()
2177 };
2178 assert_eq!(decide(&book, &still, &limits), Need::Partial(vec![steam]));
2179 let mut bare = book.clone();
2180 bare.still = false;
2181 assert_eq!(
2182 decide(&bare, &still, &limits),
2183 Need::Full,
2184 "no still layer was kept"
2185 );
2186 let gone = summary();
2187 assert_eq!(decide(&book, &gone, &limits), Need::Partial(vec![steam]));
2188 record(&mut book, gone.clone(), Shown::Partial, &limits);
2189 assert_eq!(decide(&book, &gone, &limits), Need::None);
2190 let elsewhere = Region::new(900, 500, 40, 40);
2191 record(
2192 &mut book,
2193 Summary {
2194 layered: vec![steam],
2195 ..summary()
2196 },
2197 Shown::Partial,
2198 &limits,
2199 );
2200 let moved = Summary {
2201 layered: vec![elsewhere],
2202 ..summary()
2203 };
2204 assert_eq!(
2205 decide(&book, &moved, &limits),
2206 Need::Partial(vec![steam, elsewhere])
2207 );
2208 }
2209
2210 #[test]
2211 fn a_change_settles_the_history_before_going_idle() {
2212 let limits = Limits::default();
2213 let mut book = Book::default();
2214 record(&mut book, summary(), Shown::Full, &limits);
2215 book.depth = Some(2.0);
2216 assert_eq!(book.settle, limits.settle);
2217 let mut fulls = 0;
2218 while decide(&book, &summary(), &limits) == Need::Full {
2219 record(&mut book, summary(), Shown::Full, &limits);
2220 fulls += 1;
2221 assert!(fulls <= limits.settle);
2222 }
2223 assert_eq!(fulls, limits.settle);
2224 assert_eq!(decide(&book, &summary(), &limits), Need::None);
2225 record(&mut book, turned(0.2), Shown::Reproject, &limits);
2226 record(&mut book, turned(0.7), Shown::Full, &limits);
2227 assert_eq!(book.settle, 0);
2228 assert_eq!(book.since_full, 0);
2229 let changed = Summary {
2230 world: 99,
2231 ..summary()
2232 };
2233 record(&mut book, changed, Shown::Full, &limits);
2234 assert_eq!(book.settle, limits.settle);
2235 }
2236
2237 #[test]
2238 fn prints_see_single_word_changes() {
2239 let print = |values: &[f32]| {
2240 let mut print = Print::new();
2241 print.floats(values);
2242 print.finish()
2243 };
2244 let base = print(&[1.0, 2.0, 3.0]);
2245 assert_eq!(base, print(&[1.0, 2.0, 3.0]));
2246 assert_ne!(base, print(&[1.0, 2.0, 3.0000002]));
2247 assert_ne!(base, print(&[1.0, 3.0, 2.0]));
2248 assert_ne!(base, print(&[1.0, 2.0]));
2249 let mut text = Print::new();
2250 text.debug(&"ab");
2251 let mut other = Print::new();
2252 other.debug(&"ab ");
2253 assert_ne!(text.finish(), other.finish());
2254 }
2255
2256 #[test]
2257 fn layer_rects_cover_what_they_draw() {
2258 let projection = projection(1.5);
2259 let view = view(0.0, [0.0, 1.0, 2.0]);
2260 let view_projection = multiply(projection, view);
2261 let region = box_region(
2262 &view_projection,
2263 [-0.1, 0.9, -0.1],
2264 [0.1, 1.1, 0.1],
2265 8,
2266 W,
2267 H,
2268 )
2269 .unwrap();
2270 assert!(region.contains(W / 2, H / 2));
2271 assert!(region.width < W / 4 && region.height < H / 3, "{region:?}");
2272 let behind = box_region(&view_projection, [-0.1, 0.9, 2.5], [0.1, 1.1, 2.6], 8, W, H);
2273 assert_eq!(behind, Some(Region::whole(W, H)));
2274 let particle = ParticleInstance {
2275 position_size: [0.0, 1.0, 0.0, 0.05],
2276 velocity_alpha: [0.0; 4],
2277 color_rotation: [0.0; 4],
2278 detail: [0.0; 4],
2279 };
2280 let around = particle_region(&view_projection, &[particle], 4, W, H).unwrap();
2281 assert!(around.contains(W / 2, H / 2));
2282 assert_eq!(particle_region(&view_projection, &[], 4, W, H), None);
2283 }
2284
2285 #[test]
2286 fn only_pointwise_finishes_redraw_in_rects() {
2287 use pfx_post::{Bloom, Chain, Pass, Style, Tone};
2288 let plain = Chain {
2289 passes: vec![
2290 Pass::Exposure(1.0),
2291 Pass::Bloom(Bloom::neutral(0.0)),
2292 Pass::Tone(Tone::aces()),
2293 ],
2294 frame: 0,
2295 seed: 0,
2296 };
2297 assert!(local_post(&plain));
2298 let glowing = Chain {
2299 passes: vec![Pass::Bloom(Bloom::neutral(0.4))],
2300 frame: 0,
2301 seed: 0,
2302 };
2303 assert!(!local_post(&glowing));
2304 let styled = Style::ALL
2305 .iter()
2306 .filter(|style| local_post(&style.chain()))
2307 .count();
2308 assert!(styled < Style::ALL.len());
2309 }
2310
2311 #[test]
2312 fn warp_shaders_validate() {
2313 for source in [
2314 WARP_WGSL,
2315 COPY_WGSL,
2316 WAKE_WGSL,
2317 TILES_WGSL,
2318 SPREAD_WGSL,
2319 COMPOSITE_WGSL,
2320 ] {
2321 let module = naga::front::wgsl::parse_str(source).unwrap();
2322 naga::valid::Validator::new(
2323 naga::valid::ValidationFlags::all(),
2324 naga::valid::Capabilities::all(),
2325 )
2326 .validate(&module)
2327 .unwrap();
2328 }
2329 }
2330
2331 #[test]
2332 fn warp_uniform_maps_the_cached_view_onto_itself_and_follows_a_turn() {
2333 let at = |uniform: &WarpUniform, uv: [f32; 2], depth: f32| {
2334 let toward: [f32; 4] = std::array::from_fn(|row| {
2335 uniform.across[row] * uv[0] + uniform.down[row] * uv[1] + uniform.corner[row]
2336 });
2337 let clip: [f32; 4] =
2338 std::array::from_fn(|row| toward[row] * depth + uniform.origin[row]);
2339 [clip[0] / clip[3] * 0.5 + 0.5, 0.5 - clip[1] / clip[3] * 0.5]
2340 };
2341 let still = view(0.1, [0.0, 1.0, 2.0]);
2342 let projection = projection(1.5);
2343 let same =
2344 WarpUniform::new((&still, &projection), (&still, &projection), [W, H], 40.0).unwrap();
2345 for uv in [[0.1, 0.2], [0.5, 0.5], [0.93, 0.71]] {
2346 for depth in [0.3, 2.0, 25.0] {
2347 let back = at(&same, uv, depth);
2348 assert!((back[0] - uv[0]).abs() < 1e-5 && (back[1] - uv[1]).abs() < 1e-5);
2349 }
2350 }
2351 let turned = view(0.1 + 0.5_f32.to_radians(), [0.0, 1.0, 2.0]);
2352 let warp =
2353 WarpUniform::new((&still, &projection), (&turned, &projection), [W, H], 40.0).unwrap();
2354 let near = at(&warp, [0.5, 0.5], 1.0);
2355 let far = at(&warp, [0.5, 0.5], 20.0);
2356 let shift = 0.5_f32.to_radians().tan() * 1.5 / 1.6 * 0.5;
2357 assert!((near[0] - 0.5 - shift).abs() < 1e-3, "{near:?}");
2358 assert!(
2359 (near[0] - far[0]).abs() < 1e-5,
2360 "a pure turn has no parallax"
2361 );
2362 assert_eq!(same.bytes().len(), 24 * 4);
2363 }
2364}