1use crate::canopy::{CanopyMode, Gobo, petal_emitters};
2use crate::crisp;
3use crate::demand::{self, DemandStats, Limits, Need, Next, Print, Region, Shown};
4use crate::effects::{
5 self, CreatureInstance, CreatureMesh, EffectCommands, EffectFrame, EffectTargets,
6 ParticleInstance, RoomHaze, VolumeKind, VolumeTimestamps, pack_emitter,
7};
8use crate::flat::look::{FlatLooks, Look, LookFrame};
9use crate::flat::{FlatPass, FlatScene};
10use crate::frame::{self, Frame, Instance, Matrix, MeshData, MeshHandle, OpaqueKey, Scene};
11use crate::glass;
12use crate::impostor::{ImpostorInstance, ImpostorParams, ImpostorPass};
13use crate::lights::{LightQuality, LocalLight};
14use crate::lod::{LodSet, LodStats};
15use crate::maps::{MapImages, TextureFilter};
16use crate::pipeline_cache::CacheLoad;
17use crate::probes::{Ambient, ContactDesc, ProbeLighting};
18use crate::reflect;
19use crate::shadow::{Quality, Shadows, View};
20use crate::sky::SkySource;
21use crate::stats::{FrameKind, FrameStats};
22use crate::taa::{self, GpuInputs};
23use crate::text::{GpuAtlas, PackedGlyph, QuadDraw, RichQuad, TextDraw, TextPass, TextSpace};
24use crate::warm::Warming;
25use pfx_geom::mesh::Mesh;
26use pfx_geom::tree::{PetalSource, Tree, tree_sway};
27use pfx_gpu::screens::{Budget, DynamicResolution};
28use pfx_gpu::{FrameTimings, Gpu, GpuProfiler, GpuRefusal, PassTiming, wgpu};
29use pfx_physics::{Emitter, ParticleKind, Wind};
30use pfx_post::{Bloom, Chain, Pass as PostPass, Style, Tape, Tone, dof, gpu::GpuChain};
31use pfx_text::Paragraph;
32use std::collections::VecDeque;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::mpsc::{Receiver, TryRecvError};
36use wgpu::util::DeviceExt;
37
38pub const SCENE_COLOUR_MIPS: u32 = 7;
39pub const SHADOW_RANGE: f32 = 40.0;
40pub const TIMING_LAG: u64 = 2;
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Submitted {
44 pub frame: u64,
45 pub arrived: Vec<FrameTimings>,
46}
47
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct TimingDrops {
50 pub ring: u64,
51 pub late: u64,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub enum RendererError {
56 Refused(GpuRefusal),
57 Setup(String),
58}
59
60impl std::fmt::Display for RendererError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Refused(refusal) => write!(f, "{refusal}"),
64 Self::Setup(message) => write!(f, "{message}"),
65 }
66 }
67}
68
69impl std::error::Error for RendererError {}
70
71impl From<String> for RendererError {
72 fn from(message: String) -> Self {
73 Self::Setup(message)
74 }
75}
76
77impl From<GpuRefusal> for RendererError {
78 fn from(refusal: GpuRefusal) -> Self {
79 Self::Refused(refusal)
80 }
81}
82
83impl From<RendererError> for String {
84 fn from(error: RendererError) -> Self {
85 error.to_string()
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Finish {
91 Standard,
92 Style(Style),
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
96pub enum Exposure {
97 Fixed(f32),
98 Auto { bias: f32 },
99}
100
101impl Exposure {
102 fn value(self, metered: f32) -> f32 {
103 match self {
104 Self::Fixed(value) => value,
105 Self::Auto { bias } => metered * 2.0_f32.powf(bias),
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq)]
111pub struct Picked {
112 pub x: u32,
113 pub y: u32,
114 pub id: u32,
115 pub depth: f32,
116}
117
118struct PickReadback {
119 buffer: wgpu::Buffer,
120 receiver: Receiver<Result<(), wgpu::BufferAsyncError>>,
121 x: u32,
122 y: u32,
123}
124
125pub struct TextItem<'a> {
126 pub atlas: &'a GpuAtlas,
127 pub paragraph: &'a Paragraph,
128 pub color: [f32; 4],
129 pub space: TextSpace,
130 pub id: u32,
131}
132pub struct TextQuadItem<'a> {
133 pub atlas: &'a GpuAtlas,
134 pub quads: &'a [RichQuad],
135 pub space: TextSpace,
136 pub icons: bool,
137 pub id: u32,
138}
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum TextList {
141 Surface,
142 SurfaceQuads,
143 Overlay,
144 OverlayQuads,
145}
146#[derive(Clone, Copy, Debug, PartialEq)]
147pub struct TextBounds {
148 pub list: TextList,
149 pub index: usize,
150 pub id: u32,
151 pub rect: [f32; 4],
152}
153#[derive(Default)]
154pub struct Text<'a> {
155 pub surface: Vec<TextItem<'a>>,
156 pub overlay: Vec<TextItem<'a>>,
157 pub surface_quads: Vec<TextQuadItem<'a>>,
158 pub overlay_quads: Vec<TextQuadItem<'a>>,
159}
160pub struct Volume {
161 pub kind: VolumeKind,
162 pub lo: [f32; 3],
163 pub hi: [f32; 3],
164 pub color: [f32; 3],
165 pub density: f32,
166 pub anisotropy: f32,
167 pub time_scale: f32,
168}
169pub struct Liquid<'a> {
170 pub fluid: &'a wgpu::TextureView,
171 pub ripple: &'a wgpu::TextureView,
172}
173#[derive(Default)]
174pub struct Effects<'a> {
175 pub volume: Option<Volume>,
176 pub particles: &'a [ParticleInstance],
177 pub creatures: &'a [CreatureInstance],
178 pub creature_mesh: Option<&'a CreatureMesh>,
179 pub glass: &'a [glass::Surface],
180 pub liquid: Option<Liquid<'a>>,
181 pub props: &'a [ImpostorInstance],
182}
183
184pub const HISTORY_GAP: f32 = 0.25;
185
186#[derive(Clone, Debug)]
187pub struct History {
188 previous_time: Option<f32>,
189 last_view_projection: Option<Matrix>,
190 reset: bool,
191 max_gap: f32,
192}
193
194impl Default for History {
195 fn default() -> Self {
196 Self {
197 previous_time: None,
198 last_view_projection: None,
199 reset: false,
200 max_gap: HISTORY_GAP,
201 }
202 }
203}
204
205impl History {
206 pub fn invalidate(&mut self) {
207 self.reset = true;
208 }
209
210 pub fn set_max_gap(&mut self, seconds: f32) {
211 self.max_gap = if seconds.is_finite() {
212 seconds.max(0.0)
213 } else {
214 HISTORY_GAP
215 };
216 }
217
218 pub fn max_gap(&self) -> f32 {
219 self.max_gap
220 }
221
222 pub fn advance(
223 &mut self,
224 time: f32,
225 previous_view_projection: Matrix,
226 view_projection: Matrix,
227 ) -> bool {
228 let continuous = self
229 .previous_time
230 .is_some_and(|previous| time >= previous && time - previous <= self.max_gap);
231 let chained = self
232 .last_view_projection
233 .is_some_and(|last| close(last, previous_view_projection));
234 let valid = !self.reset && continuous && chained;
235 self.reset = false;
236 self.previous_time = Some(time);
237 self.last_view_projection = Some(view_projection);
238 valid
239 }
240
241 pub fn previous_time(&self) -> Option<f32> {
242 self.previous_time
243 }
244
245 pub fn bridge(&mut self, time: f32) {
246 if self.previous_time.is_some_and(|previous| time >= previous) {
247 self.previous_time = Some(time);
248 }
249 }
250
251 pub fn last_view_projection(&self) -> Option<Matrix> {
252 self.last_view_projection
253 }
254}
255
256fn close(a: Matrix, b: Matrix) -> bool {
257 let scale = a
258 .iter()
259 .flatten()
260 .chain(b.iter().flatten())
261 .fold(1.0_f32, |high, value| high.max(value.abs()));
262 a.iter()
263 .flatten()
264 .zip(b.iter().flatten())
265 .all(|(x, y)| (x - y).abs() <= scale * 1e-4)
266}
267
268struct Petals {
269 origin: [f32; 3],
270 sources: Vec<PetalSource>,
271 emitters: Vec<Emitter>,
272 seed: u32,
273 color: [f32; 3],
274 last_time: Option<f32>,
275}
276
277impl Petals {
278 fn new(tree: &Tree, origin: [f32; 3], rate: f32, color: [f32; 3]) -> Self {
279 let emitters = petal_emitters(tree, origin, 0.0, 0.0, [1.0, 0.0, 0.0], rate);
280 let count = emitters.len();
281 let sources = (0..count)
282 .map(|i| {
283 let index = ((i as f32 + 0.5) * tree.petal_sources.len() as f32 / count as f32)
284 .floor() as usize;
285 tree.petal_sources[index.min(tree.petal_sources.len() - 1)]
286 })
287 .collect();
288 Self {
289 origin,
290 sources,
291 emitters,
292 seed: (tree.seed % 997) as u32 + 1,
293 color,
294 last_time: None,
295 }
296 }
297
298 fn step(&mut self, time: f32, wind: &crate::deform::WindFrame) -> Vec<ParticleInstance> {
299 let dt = self
300 .last_time
301 .map_or(0.0, |last| (time - last).clamp(0.0, 0.25));
302 self.last_time = Some(time);
303 let blow = Wind::new(self.seed, wind.direction, wind.strength);
304 let mut out = Vec::new();
305 for (emitter, source) in self.emitters.iter_mut().zip(&self.sources) {
306 let at = tree_sway(
307 source.position,
308 source.sway.pivot,
309 source.sway.level,
310 source.sway.stiffness,
311 wind.time,
312 wind.strength,
313 wind.direction,
314 );
315 emitter.set_origin([
316 at[0] + self.origin[0],
317 at[1] + self.origin[1],
318 at[2] + self.origin[2],
319 ]);
320 emitter.step(dt, time, &blow);
321 out.extend(pack_emitter(emitter, ParticleKind::Petal, time, self.color));
322 }
323 out
324 }
325}
326
327struct Motion {
328 layout: wgpu::BindGroupLayout,
329 pipeline: wgpu::RenderPipeline,
330 uniform: wgpu::Buffer,
331}
332
333struct Mips {
334 texture: wgpu::Texture,
335 view: wgpu::TextureView,
336 levels: Vec<wgpu::TextureView>,
337 extents: Vec<wgpu::Buffer>,
338 binds: Vec<wgpu::BindGroup>,
339 written: Option<[u32; 2]>,
340 pipeline: wgpu::RenderPipeline,
341}
342
343impl Mips {
344 fn bordered(&self, rect: [u32; 2], level: u32) -> [u32; 2] {
345 let size = self.texture.size();
346 [
347 (rect[0] + 1).min(crate::viewport::mip(size.width, level)),
348 (rect[1] + 1).min(crate::viewport::mip(size.height, level)),
349 ]
350 }
351
352 fn copy_scene(
353 &self,
354 encoder: &mut wgpu::CommandEncoder,
355 scene: &wgpu::Texture,
356 viewport: [u32; 2],
357 ) {
358 let [width, height] = viewport;
359 let copy =
360 |encoder: &mut wgpu::CommandEncoder, from: [u32; 2], to: [u32; 2], extent: [u32; 2]| {
361 encoder.copy_texture_to_texture(
362 wgpu::TexelCopyTextureInfo {
363 texture: scene,
364 mip_level: 0,
365 origin: wgpu::Origin3d {
366 x: from[0],
367 y: from[1],
368 z: 0,
369 },
370 aspect: wgpu::TextureAspect::All,
371 },
372 wgpu::TexelCopyTextureInfo {
373 texture: &self.texture,
374 mip_level: 0,
375 origin: wgpu::Origin3d {
376 x: to[0],
377 y: to[1],
378 z: 0,
379 },
380 aspect: wgpu::TextureAspect::All,
381 },
382 wgpu::Extent3d {
383 width: extent[0],
384 height: extent[1],
385 depth_or_array_layers: 1,
386 },
387 );
388 };
389 copy(encoder, [0, 0], [0, 0], [width, height]);
390 let size = self.texture.size();
391 let right = width < size.width;
392 let below = height < size.height;
393 if right {
394 copy(encoder, [width - 1, 0], [width, 0], [1, height]);
395 }
396 if below {
397 copy(encoder, [0, height - 1], [0, height], [width, 1]);
398 }
399 if right && below {
400 copy(encoder, [width - 1, height - 1], [width, height], [1, 1]);
401 }
402 }
403}
404
405struct ExposureMeter {
406 layout: wgpu::BindGroupLayout,
407 pipeline: wgpu::ComputePipeline,
408 value: wgpu::Buffer,
409 readback: wgpu::Buffer,
410 extent: wgpu::Buffer,
411 written: Option<[u32; 2]>,
412 pending: Option<Receiver<Result<(), wgpu::BufferAsyncError>>>,
413 stale: bool,
414 exposure: f32,
415 last_time: Option<f32>,
416}
417
418impl ExposureMeter {
419 fn new(device: &wgpu::Device) -> Self {
420 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
421 label: Some("live exposure layout"),
422 entries: &[
423 wgpu::BindGroupLayoutEntry {
424 binding: 0,
425 visibility: wgpu::ShaderStages::COMPUTE,
426 ty: wgpu::BindingType::Texture {
427 sample_type: wgpu::TextureSampleType::Float { filterable: false },
428 view_dimension: wgpu::TextureViewDimension::D2,
429 multisampled: false,
430 },
431 count: None,
432 },
433 wgpu::BindGroupLayoutEntry {
434 binding: 1,
435 visibility: wgpu::ShaderStages::COMPUTE,
436 ty: wgpu::BindingType::Buffer {
437 ty: wgpu::BufferBindingType::Storage { read_only: false },
438 has_dynamic_offset: false,
439 min_binding_size: None,
440 },
441 count: None,
442 },
443 wgpu::BindGroupLayoutEntry {
444 binding: 2,
445 visibility: wgpu::ShaderStages::COMPUTE,
446 ty: wgpu::BindingType::Buffer {
447 ty: wgpu::BufferBindingType::Uniform,
448 has_dynamic_offset: false,
449 min_binding_size: None,
450 },
451 count: None,
452 },
453 ],
454 });
455 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
456 label: Some("live exposure pipeline layout"),
457 bind_group_layouts: &[Some(&layout)],
458 immediate_size: 0,
459 });
460 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
461 label: Some("live exposure shader"),
462 source: wgpu::ShaderSource::Wgsl(EXPOSURE_WGSL.into()),
463 });
464 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
465 label: Some("live exposure pipeline"),
466 layout: Some(&pipeline_layout),
467 module: &module,
468 entry_point: Some("main"),
469 compilation_options: wgpu::PipelineCompilationOptions::default(),
470 cache: None,
471 });
472 let value = device.create_buffer(&wgpu::BufferDescriptor {
473 label: Some("live log luminance"),
474 size: 4,
475 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
476 mapped_at_creation: false,
477 });
478 let readback = device.create_buffer(&wgpu::BufferDescriptor {
479 label: Some("live luminance readback"),
480 size: 4,
481 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
482 mapped_at_creation: false,
483 });
484 let extent = device.create_buffer(&wgpu::BufferDescriptor {
485 label: Some("live exposure extent"),
486 size: 16,
487 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
488 mapped_at_creation: false,
489 });
490 Self {
491 layout,
492 pipeline,
493 value,
494 readback,
495 extent,
496 written: None,
497 pending: None,
498 stale: false,
499 exposure: 1.0,
500 last_time: None,
501 }
502 }
503
504 fn advance(&mut self, time: f32, valid: bool) -> f32 {
505 let seconds = self
506 .last_time
507 .map_or(1.0 / 60.0, |last| (time - last).max(0.0));
508 self.last_time = Some(time);
509 if !valid {
510 self.exposure = 1.0;
511 self.stale = self.pending.is_some();
512 }
513 if let Some(receiver) = &self.pending {
514 match receiver.try_recv() {
515 Ok(Ok(())) => {
516 let bytes = self
517 .readback
518 .slice(..)
519 .get_mapped_range()
520 .expect("a mapped range");
521 let log_luminance = f32::from_le_bytes(bytes[..4].try_into().unwrap());
522 drop(bytes);
523 self.readback.unmap();
524 self.pending = None;
525 if valid && !self.stale && log_luminance.is_finite() {
526 self.exposure = pfx_core::sky::adapt_exposure(
527 self.exposure,
528 &[log_luminance.exp()],
529 seconds,
530 );
531 }
532 self.stale = false;
533 }
534 Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
535 self.pending = None;
536 self.stale = false;
537 }
538 Err(TryRecvError::Empty) => {}
539 }
540 }
541 self.exposure
542 }
543
544 fn encode(
545 &mut self,
546 device: &wgpu::Device,
547 queue: &wgpu::Queue,
548 encoder: &mut wgpu::CommandEncoder,
549 source: &wgpu::TextureView,
550 viewport: [u32; 2],
551 ) {
552 if self.pending.is_some() {
553 return;
554 }
555 if self.written != Some(viewport) {
556 queue.write_buffer(
557 &self.extent,
558 0,
559 bytemuck::cast_slice(&[viewport[0], viewport[1], 0, 0]),
560 );
561 self.written = Some(viewport);
562 }
563 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
564 label: Some("live exposure bindings"),
565 layout: &self.layout,
566 entries: &[
567 wgpu::BindGroupEntry {
568 binding: 0,
569 resource: wgpu::BindingResource::TextureView(source),
570 },
571 wgpu::BindGroupEntry {
572 binding: 1,
573 resource: self.value.as_entire_binding(),
574 },
575 wgpu::BindGroupEntry {
576 binding: 2,
577 resource: self.extent.as_entire_binding(),
578 },
579 ],
580 });
581 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
582 label: Some("live exposure reduction"),
583 timestamp_writes: None,
584 });
585 pass.set_pipeline(&self.pipeline);
586 pass.set_bind_group(0, &bind, &[]);
587 pass.dispatch_workgroups(1, 1, 1);
588 drop(pass);
589 encoder.copy_buffer_to_buffer(&self.value, 0, &self.readback, 0, 4);
590 }
591
592 fn submitted(&mut self) {
593 if self.pending.is_some() {
594 return;
595 }
596 let (sender, receiver) = std::sync::mpsc::channel();
597 self.readback
598 .slice(..)
599 .map_async(wgpu::MapMode::Read, move |result| {
600 let _ = sender.send(result);
601 });
602 self.pending = Some(receiver);
603 }
604}
605
606const EXPOSURE_WGSL: &str = r#"
607@group(0) @binding(0) var source: texture_2d<f32>;
608@group(0) @binding(1) var<storage, read_write> output: array<f32>;
609@group(0) @binding(2) var<uniform> extent: vec4u;
610var<workgroup> values: array<f32, 256>;
611@compute @workgroup_size(256)
612fn main(@builtin(local_invocation_index) index: u32) {
613 let size = min(textureDimensions(source), extent.xy);
614 var total = 0.0;
615 for (var sample = 0u; sample < 4u; sample += 1u) {
616 let point = index * 4u + sample;
617 let x = min((point % 64u) * size.x / 64u + size.x / 128u, size.x - 1u);
618 let y = min((point / 64u) * size.y / 16u + size.y / 32u, size.y - 1u);
619 let rgb = textureLoad(source, vec2i(i32(x), i32(y)), 0).rgb;
620 total += log(max(dot(rgb, vec3f(0.2126, 0.7152, 0.0722)), 0.001));
621 }
622 values[index] = total;
623 workgroupBarrier();
624 var stride = 128u;
625 loop {
626 if (index < stride) { values[index] += values[index + stride]; }
627 workgroupBarrier();
628 if (stride == 1u) { break; }
629 stride /= 2u;
630 }
631 if (index == 0u) { output[0] = values[0] / 1024.0; }
632}
633"#;
634
635pub struct Renderer {
636 frame: Frame,
637 shadows: Shadows,
638 taa: taa::Pass,
639 crisp: crisp::Pass,
640 sharpen: f32,
641 text_after_taa: bool,
642 text: TextPass,
643 effects: effects::Effects,
644 reflections: reflect::Pass,
645 glass: glass::Pass,
646 impostors: Option<ImpostorPass>,
647 petals: Option<Petals>,
648 lod: LodSet,
649 post: GpuChain,
650 dof: dof::GpuDof,
651 lens: Option<dof::Lens>,
652 haze: Option<RoomHaze>,
653 exposure: ExposureMeter,
654 exposure_mode: Exposure,
655 custom_finish: bool,
656 tape: Option<Tape>,
657 pick_request: Option<(u32, u32)>,
658 pick_readback: Option<PickReadback>,
659 style: Finish,
660 output_format: wgpu::TextureFormat,
661 display: Option<(wgpu::Texture, wgpu::TextureView)>,
662 blit: Option<(wgpu::BindGroupLayout, wgpu::RenderPipeline)>,
663 depths: [(wgpu::Texture, wgpu::TextureView); 2],
664 depth_index: usize,
665 reactive: wgpu::Texture,
666 reactive_view: wgpu::TextureView,
667 depth_layout: wgpu::BindGroupLayout,
668 depth_pipeline: wgpu::ComputePipeline,
669 motion: Motion,
670 mips: Mips,
671 black: wgpu::TextureView,
672 profiler: GpuProfiler,
673 frames_submitted: u64,
674 frames_done: Arc<AtomicU64>,
675 submissions: VecDeque<(u64, wgpu::SubmissionIndex)>,
676 arrivals: Vec<FrameTimings>,
677 latest_timings: Option<FrameTimings>,
678 late_timings: u64,
679 stats: FrameStats,
680 resolution: Option<DynamicResolution>,
681 resolution_content: Option<[u32; 2]>,
682 resolution_frame: bool,
683 sky_hours: Vec<(u64, f32)>,
684 history: History,
685 history_valid: bool,
686 hour: f32,
687 probe_emission: f32,
688 pass_log: Vec<&'static str>,
689 text_bounds: Vec<TextBounds>,
690 flat: Option<FlatPass>,
691 flat_looks: Option<FlatLooks>,
692 plates: Option<crate::plates::Plates>,
693 plate_kit: Option<crate::plates::PlateKit>,
694 plate_casters: Vec<Instance>,
695 plate_mask: bool,
696 demand: Option<demand::Cache>,
697 revision: AtomicU64,
698 custom_local: Option<bool>,
699}
700
701pub struct GeometryReadback {
702 pub colour: Vec<[f32; 4]>,
703 pub depth: Vec<f32>,
704 pub normals: Vec<[f32; 3]>,
705}
706
707fn chain(style: Finish, encode: bool) -> Chain {
708 let mut chain = match style {
709 Finish::Standard => Chain {
710 passes: vec![
711 PostPass::Bloom(Bloom::neutral(0.0)),
712 PostPass::Tone(Tone::aces()),
713 ],
714 frame: 0,
715 seed: 0,
716 },
717 Finish::Style(style) => style.chain(),
718 };
719 chain.passes.insert(0, PostPass::Exposure(1.0));
720 if encode {
721 chain.passes.push(PostPass::Encode);
722 }
723 chain
724}
725
726fn plain_passes(passes: Vec<PassTiming>) -> Vec<PassTiming> {
727 let mut merged: Vec<PassTiming> = Vec::new();
728 for pass in passes {
729 let label = if pass.label.starts_with("post ") {
730 "post"
731 } else if pass.label.starts_with("volume ") {
732 "volume"
733 } else {
734 &pass.label
735 };
736 if let Some(existing) = merged.iter_mut().find(|item| item.label == label) {
737 existing.milliseconds += pass.milliseconds;
738 } else {
739 merged.push(PassTiming {
740 label: label.to_owned(),
741 milliseconds: pass.milliseconds,
742 });
743 }
744 }
745 merged.sort_by(|a, b| {
746 b.milliseconds
747 .total_cmp(&a.milliseconds)
748 .then_with(|| a.label.cmp(&b.label))
749 });
750 merged.truncate(16);
751 merged
752}
753
754const FULLSCREEN: &str = "@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f { let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); return vec4f(xy[index], 0.0, 1.0); }";
755
756const MOTION_WGSL: &str = r#"
757struct MotionUniform {
758 inverse: mat4x4f,
759 current: mat4x4f,
760 previous: mat4x4f,
761 size: vec4f,
762}
763@group(0) @binding(0) var scene_depth: texture_depth_2d;
764@group(0) @binding(1) var scene_ids: texture_2d<u32>;
765@group(0) @binding(2) var<uniform> motion: MotionUniform;
766@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec2f {
767 let pixel = vec2i(position.xy);
768 if (textureLoad(scene_ids, pixel, 0).r != 0u) {
769 discard;
770 }
771 let z = textureLoad(scene_depth, pixel, 0);
772 let uv = position.xy * motion.size.zw;
773 let world = motion.inverse * vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
774 let point = vec4f(world.xyz / world.w, 1.0);
775 let now = motion.current * point;
776 let before = motion.previous * point;
777 let moved = (before.xy / before.w - now.xy / now.w) * vec2f(0.5, -0.5);
778 return select(moved, vec2f(0.0), abs(moved) < vec2f(1e-8));
779}
780"#;
781
782const DOWNSAMPLE_WGSL: &str = r#"
783@group(0) @binding(0) var source: texture_2d<f32>;
784@group(0) @binding(1) var source_sampler: sampler;
785@group(0) @binding(2) var<uniform> extent: vec4u;
786@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
787 let size = vec2f(textureDimensions(source)) * 0.5;
788 let pixel = min(position.xy, vec2f(extent.xy) - vec2f(0.5));
789 return textureSampleLevel(source, source_sampler, pixel / size, 0.0);
790}
791"#;
792
793pub fn motion_source() -> String {
794 format!("{FULLSCREEN}\n{MOTION_WGSL}")
795}
796
797pub fn downsample_source() -> String {
798 format!("{FULLSCREEN}\n{DOWNSAMPLE_WGSL}")
799}
800
801fn blit_pipeline(
802 device: &wgpu::Device,
803 format: wgpu::TextureFormat,
804) -> (wgpu::BindGroupLayout, wgpu::RenderPipeline) {
805 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
806 label: Some("live output blit"),
807 entries: &[wgpu::BindGroupLayoutEntry {
808 binding: 0,
809 visibility: wgpu::ShaderStages::FRAGMENT,
810 ty: wgpu::BindingType::Texture {
811 sample_type: wgpu::TextureSampleType::Float { filterable: false },
812 view_dimension: wgpu::TextureViewDimension::D2,
813 multisampled: false,
814 },
815 count: None,
816 }],
817 });
818 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
819 label: Some("live output blit"),
820 bind_group_layouts: &[Some(&layout)],
821 immediate_size: 0,
822 });
823 let source = format!(
824 "{FULLSCREEN}\n@group(0) @binding(0) var source: texture_2d<f32>; @fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {{ return textureLoad(source, vec2i(position.xy), 0); }}"
825 );
826 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
827 label: Some("live output blit"),
828 source: wgpu::ShaderSource::Wgsl(source.into()),
829 });
830 let pipeline = fullscreen(
831 device,
832 &pipeline_layout,
833 &shader,
834 format,
835 "live output blit",
836 );
837 (layout, pipeline)
838}
839
840fn fullscreen(
841 device: &wgpu::Device,
842 layout: &wgpu::PipelineLayout,
843 shader: &wgpu::ShaderModule,
844 format: wgpu::TextureFormat,
845 label: &str,
846) -> wgpu::RenderPipeline {
847 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
848 label: Some(label),
849 layout: Some(layout),
850 vertex: wgpu::VertexState {
851 module: shader,
852 entry_point: Some("vertex"),
853 buffers: &[],
854 compilation_options: Default::default(),
855 },
856 primitive: Default::default(),
857 depth_stencil: None,
858 multisample: Default::default(),
859 fragment: Some(wgpu::FragmentState {
860 module: shader,
861 entry_point: Some("fragment"),
862 targets: &[Some(wgpu::ColorTargetState {
863 format,
864 blend: None,
865 write_mask: wgpu::ColorWrites::ALL,
866 })],
867 compilation_options: Default::default(),
868 }),
869 multiview_mask: None,
870 cache: None,
871 })
872}
873
874fn image(
875 device: &wgpu::Device,
876 width: u32,
877 height: u32,
878 format: wgpu::TextureFormat,
879 usage: wgpu::TextureUsages,
880 label: &str,
881) -> (wgpu::Texture, wgpu::TextureView) {
882 let texture = device.create_texture(&wgpu::TextureDescriptor {
883 label: Some(label),
884 size: wgpu::Extent3d {
885 width,
886 height,
887 depth_or_array_layers: 1,
888 },
889 mip_level_count: 1,
890 sample_count: 1,
891 dimension: wgpu::TextureDimension::D2,
892 format,
893 usage,
894 view_formats: &[],
895 });
896 let view = texture.create_view(&Default::default());
897 (texture, view)
898}
899
900fn linear_depths(
901 device: &wgpu::Device,
902 width: u32,
903 height: u32,
904) -> [(wgpu::Texture, wgpu::TextureView); 2] {
905 [0, 1].map(|_| {
906 image(
907 device,
908 width,
909 height,
910 wgpu::TextureFormat::R32Float,
911 wgpu::TextureUsages::STORAGE_BINDING
912 | wgpu::TextureUsages::TEXTURE_BINDING
913 | wgpu::TextureUsages::COPY_SRC,
914 "live linear depth",
915 )
916 })
917}
918
919fn depth_pipeline(device: &wgpu::Device) -> (wgpu::BindGroupLayout, wgpu::ComputePipeline) {
920 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
921 label: Some("live linear depth"),
922 entries: &[
923 wgpu::BindGroupLayoutEntry {
924 binding: 0,
925 visibility: wgpu::ShaderStages::COMPUTE,
926 ty: wgpu::BindingType::Texture {
927 sample_type: wgpu::TextureSampleType::Depth,
928 view_dimension: wgpu::TextureViewDimension::D2,
929 multisampled: false,
930 },
931 count: None,
932 },
933 wgpu::BindGroupLayoutEntry {
934 binding: 1,
935 visibility: wgpu::ShaderStages::COMPUTE,
936 ty: wgpu::BindingType::StorageTexture {
937 access: wgpu::StorageTextureAccess::WriteOnly,
938 format: wgpu::TextureFormat::R32Float,
939 view_dimension: wgpu::TextureViewDimension::D2,
940 },
941 count: None,
942 },
943 wgpu::BindGroupLayoutEntry {
944 binding: 2,
945 visibility: wgpu::ShaderStages::COMPUTE,
946 ty: wgpu::BindingType::Buffer {
947 ty: wgpu::BufferBindingType::Uniform,
948 has_dynamic_offset: false,
949 min_binding_size: None,
950 },
951 count: None,
952 },
953 ],
954 });
955 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
956 label: Some("live linear depth"),
957 bind_group_layouts: &[Some(&layout)],
958 immediate_size: 0,
959 });
960 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("live linear depth"), source: wgpu::ShaderSource::Wgsl("@group(0) @binding(0) var source: texture_depth_2d; @group(0) @binding(1) var destination: texture_storage_2d<r32float, write>; @group(0) @binding(2) var<uniform> limits: vec4f; @compute @workgroup_size(8, 8) fn main(@builtin(global_invocation_id) id: vec3u) { let size = textureDimensions(source); if (id.x >= size.x || id.y >= size.y) { return; } let z = textureLoad(source, id.xy, 0); let distance = limits.y / (z + limits.x); textureStore(destination, id.xy, vec4f(select(limits.z, distance, z < 1.0 && distance > 0.0), 0.0, 0.0, 0.0)); }".into()) });
961 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
962 label: Some("live linear depth"),
963 layout: Some(&pipeline_layout),
964 module: &shader,
965 entry_point: Some("main"),
966 compilation_options: Default::default(),
967 cache: None,
968 });
969 (layout, pipeline)
970}
971
972fn motion_pass(device: &wgpu::Device) -> Motion {
973 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
974 label: Some("live background motion"),
975 entries: &[
976 wgpu::BindGroupLayoutEntry {
977 binding: 0,
978 visibility: wgpu::ShaderStages::FRAGMENT,
979 ty: wgpu::BindingType::Texture {
980 sample_type: wgpu::TextureSampleType::Depth,
981 view_dimension: wgpu::TextureViewDimension::D2,
982 multisampled: false,
983 },
984 count: None,
985 },
986 wgpu::BindGroupLayoutEntry {
987 binding: 1,
988 visibility: wgpu::ShaderStages::FRAGMENT,
989 ty: wgpu::BindingType::Texture {
990 sample_type: wgpu::TextureSampleType::Uint,
991 view_dimension: wgpu::TextureViewDimension::D2,
992 multisampled: false,
993 },
994 count: None,
995 },
996 wgpu::BindGroupLayoutEntry {
997 binding: 2,
998 visibility: wgpu::ShaderStages::FRAGMENT,
999 ty: wgpu::BindingType::Buffer {
1000 ty: wgpu::BufferBindingType::Uniform,
1001 has_dynamic_offset: false,
1002 min_binding_size: None,
1003 },
1004 count: None,
1005 },
1006 ],
1007 });
1008 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1009 label: Some("live background motion"),
1010 bind_group_layouts: &[Some(&layout)],
1011 immediate_size: 0,
1012 });
1013 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1014 label: Some("live background motion"),
1015 source: wgpu::ShaderSource::Wgsl(motion_source().into()),
1016 });
1017 let pipeline = fullscreen(
1018 device,
1019 &pipeline_layout,
1020 &shader,
1021 wgpu::TextureFormat::Rg16Float,
1022 "live background motion",
1023 );
1024 let uniform = device.create_buffer(&wgpu::BufferDescriptor {
1025 label: Some("live background motion"),
1026 size: 208,
1027 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1028 mapped_at_creation: false,
1029 });
1030 Motion {
1031 layout,
1032 pipeline,
1033 uniform,
1034 }
1035}
1036
1037fn mips(device: &wgpu::Device, width: u32, height: u32) -> Mips {
1038 let levels_count = SCENE_COLOUR_MIPS.min(32 - width.max(height).leading_zeros());
1039 let texture = device.create_texture(&wgpu::TextureDescriptor {
1040 label: Some("live scene colour"),
1041 size: wgpu::Extent3d {
1042 width,
1043 height,
1044 depth_or_array_layers: 1,
1045 },
1046 mip_level_count: levels_count,
1047 sample_count: 1,
1048 dimension: wgpu::TextureDimension::D2,
1049 format: wgpu::TextureFormat::Rgba16Float,
1050 usage: wgpu::TextureUsages::TEXTURE_BINDING
1051 | wgpu::TextureUsages::RENDER_ATTACHMENT
1052 | wgpu::TextureUsages::COPY_DST,
1053 view_formats: &[],
1054 });
1055 let view = texture.create_view(&Default::default());
1056 let levels: Vec<wgpu::TextureView> = (0..levels_count)
1057 .map(|level| {
1058 texture.create_view(&wgpu::TextureViewDescriptor {
1059 base_mip_level: level,
1060 mip_level_count: Some(1),
1061 ..Default::default()
1062 })
1063 })
1064 .collect();
1065 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1066 label: Some("live scene colour mips"),
1067 entries: &[
1068 wgpu::BindGroupLayoutEntry {
1069 binding: 0,
1070 visibility: wgpu::ShaderStages::FRAGMENT,
1071 ty: wgpu::BindingType::Texture {
1072 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1073 view_dimension: wgpu::TextureViewDimension::D2,
1074 multisampled: false,
1075 },
1076 count: None,
1077 },
1078 wgpu::BindGroupLayoutEntry {
1079 binding: 1,
1080 visibility: wgpu::ShaderStages::FRAGMENT,
1081 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1082 count: None,
1083 },
1084 wgpu::BindGroupLayoutEntry {
1085 binding: 2,
1086 visibility: wgpu::ShaderStages::FRAGMENT,
1087 ty: wgpu::BindingType::Buffer {
1088 ty: wgpu::BufferBindingType::Uniform,
1089 has_dynamic_offset: false,
1090 min_binding_size: None,
1091 },
1092 count: None,
1093 },
1094 ],
1095 });
1096 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1097 label: Some("live scene colour mips"),
1098 bind_group_layouts: &[Some(&layout)],
1099 immediate_size: 0,
1100 });
1101 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1102 label: Some("live scene colour mips"),
1103 source: wgpu::ShaderSource::Wgsl(downsample_source().into()),
1104 });
1105 let pipeline = fullscreen(
1106 device,
1107 &pipeline_layout,
1108 &shader,
1109 wgpu::TextureFormat::Rgba16Float,
1110 "live scene colour mips",
1111 );
1112 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1113 label: Some("live scene colour mips"),
1114 mag_filter: wgpu::FilterMode::Linear,
1115 min_filter: wgpu::FilterMode::Linear,
1116 ..Default::default()
1117 });
1118 let extents: Vec<wgpu::Buffer> = (0..levels_count)
1119 .map(|_| {
1120 device.create_buffer(&wgpu::BufferDescriptor {
1121 label: Some("live scene colour mip extent"),
1122 size: 16,
1123 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1124 mapped_at_creation: false,
1125 })
1126 })
1127 .collect();
1128 let binds = (1..levels.len())
1129 .map(|level| {
1130 device.create_bind_group(&wgpu::BindGroupDescriptor {
1131 label: Some("live scene colour mips"),
1132 layout: &layout,
1133 entries: &[
1134 wgpu::BindGroupEntry {
1135 binding: 0,
1136 resource: wgpu::BindingResource::TextureView(&levels[level - 1]),
1137 },
1138 wgpu::BindGroupEntry {
1139 binding: 1,
1140 resource: wgpu::BindingResource::Sampler(&sampler),
1141 },
1142 wgpu::BindGroupEntry {
1143 binding: 2,
1144 resource: extents[level].as_entire_binding(),
1145 },
1146 ],
1147 })
1148 })
1149 .collect();
1150 Mips {
1151 texture,
1152 view,
1153 levels,
1154 extents,
1155 binds,
1156 written: None,
1157 pipeline,
1158 }
1159}
1160
1161pub fn invert(m: Matrix) -> Option<Matrix> {
1162 let a = |c: usize, r: usize| f64::from(m[c][r]);
1163 let cofactor = |c: usize, r: usize| {
1164 let rows: Vec<usize> = (0..4).filter(|&i| i != c).collect();
1165 let columns: Vec<usize> = (0..4).filter(|&i| i != r).collect();
1166 let minor = |i: usize, j: usize| a(columns[i], rows[j]);
1167 let value = minor(0, 0) * (minor(1, 1) * minor(2, 2) - minor(1, 2) * minor(2, 1))
1168 - minor(0, 1) * (minor(1, 0) * minor(2, 2) - minor(1, 2) * minor(2, 0))
1169 + minor(0, 2) * (minor(1, 0) * minor(2, 1) - minor(1, 1) * minor(2, 0));
1170 if (c + r).is_multiple_of(2) {
1171 value
1172 } else {
1173 -value
1174 }
1175 };
1176 let inverse: [[f64; 4]; 4] = std::array::from_fn(|c| std::array::from_fn(|r| cofactor(c, r)));
1177 let determinant: f64 = (0..4).map(|k| a(k, 0) * inverse[0][k]).sum();
1178 if !determinant.is_finite() || determinant.abs() < 1e-20 {
1179 return None;
1180 }
1181 Some(inverse.map(|column| column.map(|value| (value / determinant) as f32)))
1182}
1183
1184fn camera_view(scene: &Scene<'_>, width: u32, height: u32) -> View {
1185 let view = scene.camera.view;
1186 let projection = scene.camera.projection;
1187 let a = projection[2][2];
1188 let b = projection[3][2];
1189 View {
1190 eye: scene.camera.position,
1191 forward: [-view[0][2], -view[1][2], -view[2][2]],
1192 up: [view[0][1], view[1][1], view[2][1]],
1193 fov_y: 2.0 * (1.0 / projection[1][1]).atan(),
1194 aspect: width as f32 / height as f32,
1195 near: (b / a).abs().max(0.01),
1196 far: (b / (a + 1.0)).abs().clamp(1.0, SHADOW_RANGE),
1197 }
1198}
1199
1200fn solid_view(device: &wgpu::Device, queue: &wgpu::Queue, label: &str) -> wgpu::TextureView {
1201 device
1202 .create_texture_with_data(
1203 queue,
1204 &wgpu::TextureDescriptor {
1205 label: Some(label),
1206 size: wgpu::Extent3d {
1207 width: 1,
1208 height: 1,
1209 depth_or_array_layers: 1,
1210 },
1211 mip_level_count: 1,
1212 sample_count: 1,
1213 dimension: wgpu::TextureDimension::D2,
1214 format: wgpu::TextureFormat::Rgba16Float,
1215 usage: wgpu::TextureUsages::TEXTURE_BINDING,
1216 view_formats: &[],
1217 },
1218 wgpu::util::TextureDataOrder::LayerMajor,
1219 &[0; 8],
1220 )
1221 .create_view(&Default::default())
1222}
1223
1224impl Renderer {
1225 pub fn new(gpu: Gpu, width: u32, height: u32) -> Result<Self, RendererError> {
1226 Self::new_with_output_format(gpu, width, height, wgpu::TextureFormat::Rgba16Float)
1227 }
1228
1229 pub fn new_with_output_format(
1230 gpu: Gpu,
1231 width: u32,
1232 height: u32,
1233 output_format: wgpu::TextureFormat,
1234 ) -> Result<Self, RendererError> {
1235 gpu.check_floor()?;
1236 if width == 0 || height == 0 {
1237 return Err(String::from("viewport is empty").into());
1238 }
1239 if !matches!(
1240 output_format,
1241 wgpu::TextureFormat::Rgba16Float
1242 | wgpu::TextureFormat::Rgba8UnormSrgb
1243 | wgpu::TextureFormat::Bgra8UnormSrgb
1244 ) {
1245 return Err(String::from("unsupported output format").into());
1246 }
1247 let device = &gpu.device;
1248 let shadows = Shadows::new(device, Quality::default());
1249 let mut taa = taa::Pass::new(device);
1250 taa.resize(device, width, height);
1251 let text = TextPass::new(device);
1252 let effects = effects::Effects::new(device, width, height, 4)?;
1253 let style = Finish::Standard;
1254 let post = GpuChain::new(
1255 chain(style, output_format == wgpu::TextureFormat::Rgba16Float),
1256 device,
1257 width,
1258 height,
1259 );
1260 let display = (output_format != wgpu::TextureFormat::Rgba16Float).then(|| {
1261 image(
1262 device,
1263 width,
1264 height,
1265 wgpu::TextureFormat::Rgba16Float,
1266 wgpu::TextureUsages::STORAGE_BINDING
1267 | wgpu::TextureUsages::RENDER_ATTACHMENT
1268 | wgpu::TextureUsages::TEXTURE_BINDING,
1269 "live display intermediate",
1270 )
1271 });
1272 let blit = display
1273 .as_ref()
1274 .map(|_| blit_pipeline(device, output_format));
1275 let (reactive, reactive_view) = image(
1276 device,
1277 width,
1278 height,
1279 wgpu::TextureFormat::R8Unorm,
1280 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1281 "live reactive mask",
1282 );
1283 let (depth_layout, depth_pipeline) = depth_pipeline(device);
1284 let profiler = GpuProfiler::new(device, &gpu.queue);
1285 let black = solid_view(device, &gpu.queue, "live empty input");
1286 let mut renderer = Self {
1287 shadows,
1288 taa,
1289 crisp: crisp::Pass::new(device, width, height),
1290 sharpen: 0.0,
1291 text_after_taa: true,
1292 text,
1293 effects,
1294 reflections: reflect::Pass::new(device, width, height),
1295 glass: glass::Pass::new(device, width, height),
1296 impostors: None,
1297 petals: None,
1298 lod: LodSet::new(2_000_000),
1299 post,
1300 dof: dof::GpuDof::new(device, width, height),
1301 lens: None,
1302 haze: None,
1303 exposure: ExposureMeter::new(device),
1304 exposure_mode: Exposure::Auto { bias: 0.0 },
1305 custom_finish: false,
1306 tape: None,
1307 pick_request: None,
1308 pick_readback: None,
1309 style,
1310 output_format,
1311 display,
1312 blit,
1313 depths: linear_depths(device, width, height),
1314 depth_index: 0,
1315 reactive,
1316 reactive_view,
1317 depth_layout,
1318 depth_pipeline,
1319 motion: motion_pass(device),
1320 mips: mips(device, width, height),
1321 black,
1322 profiler,
1323 frames_submitted: 0,
1324 frames_done: Arc::new(AtomicU64::new(0)),
1325 submissions: VecDeque::new(),
1326 arrivals: Vec::new(),
1327 latest_timings: None,
1328 late_timings: 0,
1329 stats: FrameStats::from_env(),
1330 resolution: None,
1331 resolution_content: None,
1332 resolution_frame: false,
1333 sky_hours: Vec::new(),
1334 history: History::default(),
1335 history_valid: false,
1336 hour: 12.0,
1337 probe_emission: 0.0,
1338 pass_log: Vec::new(),
1339 text_bounds: Vec::new(),
1340 flat: None,
1341 flat_looks: None,
1342 plates: None,
1343 plate_kit: None,
1344 plate_casters: Vec::new(),
1345 plate_mask: false,
1346 demand: None,
1347 revision: AtomicU64::new(0),
1348 custom_local: None,
1349 frame: Frame::new(gpu, width, height)?,
1350 };
1351 renderer.frame.set_reflections_planned(true);
1352 Ok(renderer)
1353 }
1354
1355 pub fn gpu(&self) -> &Gpu {
1356 &self.frame.gpu
1357 }
1358
1359 pub fn size(&self) -> (u32, u32) {
1360 let [width, height] = self.frame.viewport();
1361 (width, height)
1362 }
1363
1364 pub fn capacity(&self) -> (u32, u32) {
1365 (self.frame.targets.width, self.frame.targets.height)
1366 }
1367
1368 pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
1369 let (capacity_width, capacity_height) = self.capacity();
1370 crate::viewport::check([width, height], [capacity_width, capacity_height])?;
1371 if self.size() == (width, height) {
1372 return Ok(());
1373 }
1374 if self.demand.is_some() && (width, height) != self.capacity() {
1375 return Err("frames on demand render at the full size; turn them off to render into a smaller viewport".into());
1376 }
1377 self.touch();
1378 self.set_viewport_parts(width, height)
1379 }
1380
1381 fn set_viewport_parts(&mut self, width: u32, height: u32) -> Result<(), String> {
1382 self.frame.set_viewport(width, height)?;
1383 self.taa.set_viewport(width, height)?;
1384 self.crisp.set_viewport(width, height)?;
1385 self.effects.set_viewport(width, height)?;
1386 self.reflections.set_viewport(width, height)?;
1387 self.glass.set_viewport(width, height)?;
1388 self.post.set_viewport(width, height)?;
1389 self.dof.set_viewport(width, height)?;
1390 self.text_bounds.clear();
1391 if self
1392 .pick_request
1393 .is_some_and(|(x, y)| x >= width || y >= height)
1394 {
1395 self.pick_request = None;
1396 }
1397 Ok(())
1398 }
1399
1400 pub fn history_restarts(&self) -> u64 {
1401 self.taa.restarts()
1402 }
1403
1404 pub fn target_textures(&self) -> Vec<wgpu::Texture> {
1405 let targets = &self.frame.targets;
1406 let mut textures = vec![
1407 targets.hdr.texture.clone(),
1408 targets.depth.clone(),
1409 targets.velocity.clone(),
1410 targets.ids.clone(),
1411 targets.normal_roughness.clone(),
1412 self.reactive.clone(),
1413 self.mips.texture.clone(),
1414 self.depths[0].0.clone(),
1415 self.depths[1].0.clone(),
1416 ];
1417 textures.extend(self.taa.textures());
1418 textures.extend(self.crisp.target().cloned());
1419 textures.extend(self.effects.textures());
1420 textures.extend(self.reflections.textures());
1421 textures.extend(self.glass.textures());
1422 textures.extend(self.display.as_ref().map(|(texture, _)| texture.clone()));
1423 textures.extend(self.flat.iter().flat_map(FlatPass::target_textures));
1424 textures
1425 }
1426
1427 fn post_chain(&self, finish: Chain) -> Result<GpuChain, String> {
1428 let (capacity_width, capacity_height) = self.capacity();
1429 let (width, height) = self.size();
1430 let mut post = GpuChain::new(
1431 finish,
1432 &self.frame.gpu.device,
1433 capacity_width,
1434 capacity_height,
1435 );
1436 post.set_viewport(width, height)?;
1437 Ok(post)
1438 }
1439
1440 pub fn output_format(&self) -> wgpu::TextureFormat {
1441 self.output_format
1442 }
1443
1444 pub fn last_pass_order(&self) -> &[&'static str] {
1445 &self.pass_log
1446 }
1447
1448 pub fn set_history_gap(&mut self, seconds: f32) {
1449 self.history.set_max_gap(seconds);
1450 }
1451
1452 pub fn history_valid(&self) -> bool {
1453 self.history_valid
1454 }
1455
1456 pub fn frame(&self) -> &Frame {
1457 &self.frame
1458 }
1459
1460 pub fn warm(&mut self, scenes: &[Scene<'_>]) -> Result<Warming, String> {
1461 let (width, height) = self.size();
1462 let mut keys: Vec<Vec<OpaqueKey>> = Vec::with_capacity(scenes.len());
1463 for scene in scenes {
1464 let fit = self
1465 .shadows
1466 .fit(&camera_view(scene, width, height), scene.sun.direction);
1467 keys.push(self.frame.opaque_keys(scene, Some((&self.shadows, &fit)))?);
1468 }
1469 let first = keys.first().cloned().unwrap_or_default();
1470 let later: Vec<OpaqueKey> = keys.into_iter().skip(1).flatten().collect();
1471 Ok(self.frame.warm(&first, &later))
1472 }
1473
1474 pub fn set_warm_workers(&mut self, workers: usize) {
1475 self.frame.set_warm_workers(workers);
1476 }
1477
1478 pub fn use_pipeline_cache(&mut self, path: &std::path::Path) -> CacheLoad {
1479 self.frame.use_pipeline_cache(path)
1480 }
1481
1482 pub fn save_pipeline_cache(&self) -> Result<Option<usize>, String> {
1483 self.frame.save_pipeline_cache()
1484 }
1485
1486 pub fn warm_opaque(&mut self, features: frame::OpaqueFeatures, lit: bool) {
1487 self.frame.warm_opaque(features, lit);
1488 }
1489
1490 pub fn set_finish(&mut self, mut finish: Chain) {
1491 self.touch();
1492 finish.passes.insert(0, PostPass::Exposure(1.0));
1493 if self.display.is_none() {
1494 finish.passes.push(PostPass::Encode);
1495 }
1496 self.custom_local = Some(demand::local_post(&finish));
1497 if let Ok(post) = self.post_chain(finish) {
1498 self.post = post;
1499 }
1500 self.custom_finish = true;
1501 }
1502
1503 pub fn set_tape(&mut self, tape: Option<Tape>) {
1504 if tape != self.tape {
1505 self.touch();
1506 }
1507 self.tape = tape;
1508 match tape {
1509 Some(tape) => self.post.set_tape(tape),
1510 None => self.post.reset_tape(),
1511 }
1512 }
1513
1514 pub fn clear_finish(&mut self) {
1515 self.touch();
1516 self.custom_finish = false;
1517 self.custom_local = None;
1518 if let Ok(post) = self.post_chain(chain(self.style, self.display.is_none())) {
1519 self.post = post;
1520 }
1521 }
1522
1523 pub fn set_text_after_taa(&mut self, after: bool) {
1524 if after != self.text_after_taa {
1525 self.touch();
1526 }
1527 self.text_after_taa = after;
1528 }
1529
1530 pub fn text_after_taa(&self) -> bool {
1531 self.text_after_taa
1532 }
1533
1534 pub fn set_sharpen(&mut self, strength: f32) -> Result<(), String> {
1535 let strength = crisp::check_strength(strength)?;
1536 if strength != self.sharpen {
1537 self.touch();
1538 }
1539 self.sharpen = strength;
1540 Ok(())
1541 }
1542
1543 pub fn sharpen(&self) -> f32 {
1544 self.sharpen
1545 }
1546
1547 pub fn set_texture_filter(&mut self, filter: TextureFilter) -> Result<(), String> {
1548 if filter != self.frame.texture_filter() {
1549 self.touch();
1550 }
1551 self.frame.set_texture_filter(filter)
1552 }
1553
1554 pub fn texture_filter(&self) -> TextureFilter {
1555 self.frame.texture_filter()
1556 }
1557
1558 pub fn set_lens(&mut self, lens: Option<dof::Lens>) {
1559 let lens = lens.filter(|lens| lens.enabled());
1560 if lens != self.lens {
1561 self.touch();
1562 }
1563 self.lens = lens;
1564 }
1565
1566 pub fn set_haze(&mut self, haze: Option<RoomHaze>) {
1567 if haze != self.haze {
1568 self.touch();
1569 }
1570 self.haze = haze;
1571 }
1572
1573 pub fn set_exposure(&mut self, exposure: Exposure) -> Result<(), String> {
1574 if exposure != self.exposure_mode {
1575 self.touch();
1576 }
1577 match exposure {
1578 Exposure::Fixed(value) if !value.is_finite() || value <= 0.0 => {
1579 return Err("fixed exposure must be finite and positive".into());
1580 }
1581 Exposure::Auto { bias }
1582 if !bias.is_finite()
1583 || !(f32::MIN_POSITIVE..=f32::MAX / 8.0).contains(&2.0_f32.powf(bias)) =>
1584 {
1585 return Err("exposure bias must have a finite multiplier".into());
1586 }
1587 _ => {}
1588 }
1589 if self.exposure_mode != exposure {
1590 self.exposure.exposure = 1.0;
1591 self.exposure.stale = self.exposure.pending.is_some();
1592 }
1593 self.exposure_mode = exposure;
1594 Ok(())
1595 }
1596
1597 pub fn pick(&mut self, x: u32, y: u32) -> Result<(), String> {
1598 let (width, height) = self.size();
1599 if x >= width || y >= height {
1600 return Err("pick pixel is outside the viewport".into());
1601 }
1602 self.pick_request = Some((x, y));
1603 Ok(())
1604 }
1605
1606 pub fn picked(&mut self) -> Option<Picked> {
1607 let readback = self.pick_readback.as_ref()?;
1608 let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
1609 let result = readback.receiver.try_recv();
1610 match result {
1611 Ok(Ok(())) => {
1612 let bytes = readback
1613 .buffer
1614 .slice(..)
1615 .get_mapped_range()
1616 .expect("a mapped range");
1617 let id = u32::from_le_bytes(bytes[..4].try_into().unwrap());
1618 let depth = f32::from_le_bytes(bytes[256..260].try_into().unwrap());
1619 drop(bytes);
1620 readback.buffer.unmap();
1621 let picked = Picked {
1622 x: readback.x,
1623 y: readback.y,
1624 id,
1625 depth,
1626 };
1627 self.pick_readback = None;
1628 Some(picked)
1629 }
1630 Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
1631 self.pick_readback = None;
1632 None
1633 }
1634 Err(TryRecvError::Empty) => None,
1635 }
1636 }
1637
1638 pub fn text_bounds(&self) -> &[TextBounds] {
1639 &self.text_bounds
1640 }
1641
1642 pub fn text_at(&self, x: f32, y: f32) -> Option<&TextBounds> {
1643 self.text_bounds.iter().rev().find(|bounds| {
1644 let [left, top, right, bottom] = bounds.rect;
1645 (left..right).contains(&x) && (top..bottom).contains(&y)
1646 })
1647 }
1648
1649 pub fn readback_geometry(&self) -> Result<GeometryReadback, String> {
1650 let targets = &self.frame.targets;
1651 let gpu = &self.frame.gpu;
1652 let [width, height] = self.frame.viewport();
1653 let colour = readback_texture(gpu, &targets.hdr.texture, width, height, 8)?;
1654 let depth = readback_texture(gpu, &targets.depth, width, height, 4)?;
1655 let normal = readback_texture(gpu, &targets.normal_roughness, width, height, 8)?;
1656 let colour = colour
1657 .chunks_exact(8)
1658 .map(|pixel| {
1659 std::array::from_fn(|i| {
1660 half::f16::from_le_bytes(pixel[i * 2..i * 2 + 2].try_into().unwrap()).to_f32()
1661 })
1662 })
1663 .collect();
1664 let depth = depth
1665 .chunks_exact(4)
1666 .map(|pixel| f32::from_le_bytes(pixel.try_into().unwrap()))
1667 .collect();
1668 let normal = normal
1669 .chunks_exact(8)
1670 .map(|pixel| {
1671 std::array::from_fn(|i| {
1672 half::f16::from_le_bytes(pixel[i * 2..i * 2 + 2].try_into().unwrap()).to_f32()
1673 })
1674 })
1675 .collect();
1676 Ok(GeometryReadback {
1677 colour,
1678 depth,
1679 normals: normal,
1680 })
1681 }
1682
1683 pub fn shadows(&self) -> &Shadows {
1684 &self.shadows
1685 }
1686
1687 pub fn set_shadow_quality(&mut self, quality: Quality) {
1688 self.touch();
1689 self.shadows.update_quality(&self.frame.gpu.device, quality);
1690 }
1691
1692 pub fn upload_mesh(&mut self, data: MeshData<'_>) -> Result<MeshHandle, String> {
1693 self.touch();
1694 self.frame.upload_mesh(data)
1695 }
1696
1697 pub fn upload_skinned_mesh(
1698 &mut self,
1699 data: MeshData<'_>,
1700 skin: crate::skin::SkinData<'_>,
1701 ) -> Result<MeshHandle, String> {
1702 self.touch();
1703 self.frame.upload_skinned_mesh(data, skin)
1704 }
1705
1706 pub fn set_poses(&mut self, poses: &[crate::skin::InstancePose<'_>]) -> Result<(), String> {
1707 if self.frame.same_poses(poses) {
1708 return Ok(());
1709 }
1710 self.touch();
1711 self.frame.set_poses(poses)
1712 }
1713
1714 pub fn set_surfaces(&mut self, surfaces: &[frame::InstanceSurface]) -> Result<(), String> {
1715 if self.frame.surfaces().as_slice() == surfaces {
1716 return Ok(());
1717 }
1718 self.touch();
1719 self.frame.set_surfaces(surfaces)
1720 }
1721
1722 pub fn release_mesh(&mut self, handle: MeshHandle) -> Result<(), String> {
1723 self.touch();
1724 self.frame.release_mesh(handle)
1725 }
1726
1727 pub fn replace_mesh(&mut self, handle: MeshHandle, data: MeshData<'_>) -> Result<(), String> {
1728 self.touch();
1729 self.frame.replace_mesh(handle, data)?;
1730 self.history.invalidate();
1731 Ok(())
1732 }
1733
1734 pub fn mesh_stats(&self) -> frame::MeshStats {
1735 let glass = self.glass.mesh_stats();
1736 frame::MeshStats {
1737 glass_live: glass.live,
1738 glass_slots: glass.slots,
1739 glass_buffers: glass.buffers,
1740 ..self.frame.mesh_stats()
1741 }
1742 }
1743
1744 pub fn glass_mesh_buffers(
1745 &self,
1746 handle: glass::MeshHandle,
1747 ) -> Option<(&wgpu::Buffer, &wgpu::Buffer, u32)> {
1748 self.glass.mesh_buffers(handle)
1749 }
1750
1751 pub fn upload_glass(&mut self, mesh: &Mesh) -> Result<glass::MeshHandle, String> {
1752 self.touch();
1753 self.glass.upload_mesh(&self.frame.gpu.device, mesh)
1754 }
1755
1756 pub fn release_glass(&mut self, handle: glass::MeshHandle) -> Result<(), String> {
1757 self.touch();
1758 self.glass.release_mesh(handle)
1759 }
1760
1761 pub fn replace_glass(&mut self, handle: glass::MeshHandle, mesh: &Mesh) -> Result<(), String> {
1762 self.touch();
1763 let gpu = &self.frame.gpu;
1764 self.glass
1765 .replace_mesh(&gpu.device, &gpu.queue, handle, mesh)?;
1766 self.history.invalidate();
1767 Ok(())
1768 }
1769
1770 pub fn lod_mut(&mut self) -> &mut LodSet {
1771 self.touch();
1772 &mut self.lod
1773 }
1774
1775 pub fn lod_stats(&self) -> LodStats {
1776 self.lod.stats()
1777 }
1778
1779 pub fn set_sky(&mut self, source: SkySource, hour: f32) -> Result<(), String> {
1780 self.touch();
1781 if !hour.is_finite() {
1782 return Err("hour must be finite".into());
1783 }
1784 self.frame.set_sky(source, true);
1785 self.sky_hours.clear();
1786 self.frame.set_probe_hour(hour)?;
1787 self.hour = hour;
1788 self.history.invalidate();
1789 Ok(())
1790 }
1791
1792 pub fn request_sky(&mut self, source: SkySource, hour: f32) -> Result<u64, String> {
1793 self.touch();
1794 if !hour.is_finite() {
1795 return Err("hour must be finite".into());
1796 }
1797 let request = self.frame.set_sky(source, false);
1798 self.sky_hours.push((request, hour));
1799 Ok(request)
1800 }
1801
1802 pub fn sky_settling(&self) -> bool {
1803 self.frame.sky().settling()
1804 }
1805
1806 pub fn wait_sky(&mut self) -> Result<bool, String> {
1807 match self.frame.sky_mut().wait_settled() {
1808 Some(shown) => {
1809 self.sky_shown(shown)?;
1810 Ok(true)
1811 }
1812 None => Ok(false),
1813 }
1814 }
1815
1816 fn sky_shown(&mut self, shown: u64) -> Result<(), String> {
1817 self.touch();
1818 if let Some(&(_, hour)) = self.sky_hours.iter().find(|(request, _)| *request == shown) {
1819 self.frame.set_probe_hour(hour)?;
1820 self.hour = hour;
1821 }
1822 self.sky_hours.retain(|(request, _)| *request > shown);
1823 self.history.invalidate();
1824 Ok(())
1825 }
1826
1827 pub fn set_probes(&mut self, probes: ProbeLighting) -> Result<(), String> {
1828 self.touch();
1829 self.frame.set_probes(probes);
1830 self.frame.set_probe_hour(self.hour)?;
1831 self.frame.set_probe_emission(self.probe_emission)
1832 }
1833
1834 pub fn set_ambient(&mut self, ambient: Ambient) -> Result<(), String> {
1835 self.touch();
1836 self.frame.set_ambient(ambient)?;
1837 self.history.invalidate();
1838 Ok(())
1839 }
1840
1841 pub fn set_contact(&mut self, desc: Option<ContactDesc>) -> Result<(), String> {
1842 self.touch();
1843 self.frame.set_contact(desc)?;
1844 self.history.invalidate();
1845 Ok(())
1846 }
1847
1848 pub fn write_contact(&self, values: &[f32]) -> Result<(), String> {
1849 self.touch();
1850 self.frame.write_contact(values)
1851 }
1852
1853 pub fn set_reflection_occlusion(&mut self, threshold: f32) -> Result<(), String> {
1854 self.touch();
1855 self.frame.set_reflection_occlusion(threshold)
1856 }
1857
1858 pub fn set_sky_visibility(&mut self, on: bool) {
1859 self.touch();
1860 self.frame.set_sky_visibility(on);
1861 }
1862
1863 pub fn set_probe_emission(&mut self, scale: f32) -> Result<(), String> {
1864 self.touch();
1865 self.frame.set_probe_emission(scale)?;
1866 self.probe_emission = scale;
1867 Ok(())
1868 }
1869
1870 pub fn set_lights(&mut self, lights: &[LocalLight]) -> Result<(), String> {
1871 self.touch();
1872 self.frame.set_lights(lights)
1873 }
1874
1875 pub fn set_light_quality(&mut self, quality: LightQuality) -> Result<(), String> {
1876 self.touch();
1877 self.frame.set_light_quality(quality)
1878 }
1879
1880 pub fn set_transmission(&mut self, tints: &[Option<[f32; 3]>]) -> Result<(), String> {
1881 self.touch();
1882 self.frame.set_transmission(tints)
1883 }
1884
1885 pub fn set_maps(&mut self, images: &MapImages<'_>) -> Result<(), String> {
1886 self.touch();
1887 self.frame.set_maps(images)
1888 }
1889
1890 pub fn set_detail(&mut self, atlas: &pfx_bake::detail::atlas::Atlas) -> Result<(), String> {
1891 self.touch();
1892 self.frame.set_detail(atlas)
1893 }
1894
1895 pub fn set_plates(
1896 &mut self,
1897 plate: Option<&pfx_bake::plate::Plate>,
1898 settings: crate::plates::PlateSettings,
1899 ) -> Result<(), String> {
1900 let plates = match plate {
1901 Some(plate) => {
1902 let kit = self.plate_kit();
1903 Some(crate::plates::Plates::with_kit(
1904 &self.frame.gpu,
1905 &kit,
1906 plate,
1907 settings,
1908 )?)
1909 }
1910 None => None,
1911 };
1912 self.swap_plates(plates);
1913 Ok(())
1914 }
1915
1916 pub fn plate_kit(&mut self) -> crate::plates::PlateKit {
1917 let gpu = &self.frame.gpu;
1918 let cache = self.frame.pipeline_cache();
1919 self.plate_kit
1920 .get_or_insert_with(|| crate::plates::PlateKit::new(gpu, cache))
1921 .clone()
1922 }
1923
1924 pub fn swap_plates(
1925 &mut self,
1926 plates: Option<crate::plates::Plates>,
1927 ) -> Option<crate::plates::Plates> {
1928 self.touch();
1929 let plates = plates.map(|mut plates| {
1930 plates.set_hour(self.hour);
1931 plates
1932 });
1933 let old = std::mem::replace(&mut self.plates, plates);
1934 self.taa.invalidate();
1935 old
1936 }
1937
1938 pub fn plates(&self) -> Option<&crate::plates::Plates> {
1939 self.plates.as_ref()
1940 }
1941
1942 pub fn set_plate_settings(&mut self, settings: crate::plates::PlateSettings) {
1943 if let Some(plates) = &mut self.plates
1944 && plates.settings() != settings
1945 {
1946 plates.set_settings(settings);
1947 self.touch();
1948 }
1949 }
1950
1951 pub fn set_plate_hour(&mut self, hour: f32) {
1952 if let Some(plates) = &mut self.plates
1953 && plates.hour().to_bits() != hour.to_bits()
1954 {
1955 plates.set_hour(hour);
1956 self.touch();
1957 }
1958 }
1959
1960 pub fn set_plate_casters(&mut self, casters: &[Instance]) {
1961 self.touch();
1962 self.plate_casters = casters
1963 .iter()
1964 .map(|caster| Instance {
1965 shadow_only: true,
1966 previous_model: caster.model,
1967 ..*caster
1968 })
1969 .collect();
1970 }
1971
1972 pub fn plate_casters(&self) -> &[Instance] {
1973 &self.plate_casters
1974 }
1975
1976 pub fn set_props(&mut self, props: Option<ImpostorPass>) {
1977 self.touch();
1978 self.impostors = props;
1979 }
1980
1981 pub fn set_tree(
1982 &mut self,
1983 tree: &Tree,
1984 origin: [f32; 3],
1985 receiver_y: f32,
1986 id: u32,
1987 petal_rate: f32,
1988 petal_color: [f32; 3],
1989 ) -> Result<MeshHandle, String> {
1990 self.touch();
1991 let handle = self.frame.set_tree(tree, origin, receiver_y, id)?;
1992 self.petals = Some(Petals::new(tree, origin, petal_rate, petal_color));
1993 Ok(handle)
1994 }
1995
1996 pub fn set_canopy_mode(&mut self, mode: CanopyMode) -> Result<(), String> {
1997 self.touch();
1998 self.frame.set_canopy_mode(mode)
1999 }
2000
2001 pub fn set_canopy_sharpness(&mut self, sharpness: f32) -> Result<(), String> {
2002 self.touch();
2003 self.frame.set_canopy_sharpness(sharpness)
2004 }
2005
2006 pub fn set_canopy_strength(&mut self, strength: f32) -> Result<(), String> {
2007 self.touch();
2008 self.frame.set_canopy_strength(strength)
2009 }
2010
2011 pub fn set_canopy_gobo(&mut self, gobo: Gobo) -> Result<(), String> {
2012 self.touch();
2013 self.frame.set_canopy_gobo(gobo)?;
2014 self.text.set_canopy_gobo(gobo);
2015 Ok(())
2016 }
2017
2018 pub fn volume_cuts(&self) -> effects::VolumeCuts {
2019 self.effects.volume_cuts()
2020 }
2021
2022 pub fn set_volume_cuts(&mut self, cuts: effects::VolumeCuts) {
2023 self.touch();
2024 let device = self.frame.gpu.device.clone();
2025 self.effects.set_volume_cuts(&device, cuts);
2026 }
2027
2028 pub fn resize(&mut self, width: u32, height: u32) -> Result<(), String> {
2029 self.touch();
2030 self.frame.resize(width, height)?;
2031 let owned_device = self.frame.gpu.device.clone();
2032 let device = &owned_device;
2033 self.taa.resize(device, width, height);
2034 self.crisp.resize(width, height);
2035 let cuts = self.effects.volume_cuts();
2036 self.effects = effects::Effects::new(device, width, height, 4)?;
2037 self.effects.set_volume_cuts(device, cuts);
2038 self.post.resize(width, height);
2039 self.dof.resize(width, height);
2040 self.reflections.resize(device, width, height);
2041 let meshes = self.glass.take_meshes();
2042 self.glass = glass::Pass::new(device, width, height);
2043 self.glass.set_meshes(meshes);
2044 if self.display.is_some() {
2045 self.display = Some(image(
2046 device,
2047 width,
2048 height,
2049 wgpu::TextureFormat::Rgba16Float,
2050 wgpu::TextureUsages::STORAGE_BINDING
2051 | wgpu::TextureUsages::RENDER_ATTACHMENT
2052 | wgpu::TextureUsages::TEXTURE_BINDING,
2053 "live display intermediate",
2054 ));
2055 }
2056 self.depths = linear_depths(device, width, height);
2057 self.mips = mips(device, width, height);
2058 (self.reactive, self.reactive_view) = image(
2059 device,
2060 width,
2061 height,
2062 wgpu::TextureFormat::R8Unorm,
2063 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2064 "live reactive mask",
2065 );
2066 if let Some(limits) = self.frames_on_demand() {
2067 self.demand = Some(demand::Cache::new(device, [width, height], limits));
2068 }
2069 self.history.invalidate();
2070 self.history_valid = false;
2071 self.pass_log.clear();
2072 self.text_bounds.clear();
2073 self.pick_request = None;
2074 self.pick_readback = None;
2075 self.set_viewport_parts(width, height)
2076 }
2077
2078 pub fn render(
2079 &mut self,
2080 scene: &Scene<'_>,
2081 text: &Text<'_>,
2082 effects: &Effects<'_>,
2083 style: Finish,
2084 output: &wgpu::TextureView,
2085 ) -> Result<Vec<PassTiming>, String> {
2086 let frame = self.encode_frame(scene, text, effects, style, None, output, false)?;
2087 self.wait_for(frame)
2088 }
2089
2090 pub fn render_with_flat(
2091 &mut self,
2092 scene: &Scene<'_>,
2093 text: &Text<'_>,
2094 effects: &Effects<'_>,
2095 style: Finish,
2096 flat: &FlatScene<'_>,
2097 output: &wgpu::TextureView,
2098 ) -> Result<Vec<PassTiming>, String> {
2099 let frame = self.encode_frame(scene, text, effects, style, Some(flat), output, false)?;
2100 self.wait_for(frame)
2101 }
2102
2103 pub fn render_flat(
2104 &mut self,
2105 flat: &FlatScene<'_>,
2106 output: &wgpu::TextureView,
2107 ) -> Result<Vec<PassTiming>, String> {
2108 let frame = self.encode_flat_frame(flat, None, output)?;
2109 self.wait_for(frame)
2110 }
2111
2112 pub fn render_flat_look(
2113 &mut self,
2114 flat: &FlatScene<'_>,
2115 look: &LookFrame<'_>,
2116 output: &wgpu::TextureView,
2117 ) -> Result<Vec<PassTiming>, String> {
2118 let frame = self.encode_flat_frame(flat, Some(look), output)?;
2119 self.wait_for(frame)
2120 }
2121
2122 fn wait_for(&mut self, frame: u64) -> Result<Vec<PassTiming>, String> {
2123 self.frame
2124 .gpu
2125 .device
2126 .poll(wgpu::PollType::wait_indefinitely())
2127 .map_err(|error| format!("{error:?}"))?;
2128 self.gather_timings(self.frames_submitted.saturating_sub(1));
2129 Ok(std::mem::take(&mut self.arrivals)
2130 .into_iter()
2131 .find(|timings| timings.frame == frame)
2132 .map(|timings| timings.passes)
2133 .unwrap_or_default())
2134 }
2135
2136 pub fn submit(
2137 &mut self,
2138 scene: &Scene<'_>,
2139 text: &Text<'_>,
2140 effects: &Effects<'_>,
2141 style: Finish,
2142 output: &wgpu::TextureView,
2143 ) -> Result<Submitted, String> {
2144 let frame = self.encode_frame(scene, text, effects, style, None, output, false)?;
2145 Ok(self.submitted_frame(frame))
2146 }
2147
2148 pub fn submit_with_flat(
2149 &mut self,
2150 scene: &Scene<'_>,
2151 text: &Text<'_>,
2152 effects: &Effects<'_>,
2153 style: Finish,
2154 flat: &FlatScene<'_>,
2155 output: &wgpu::TextureView,
2156 ) -> Result<Submitted, String> {
2157 let frame = self.encode_frame(scene, text, effects, style, Some(flat), output, false)?;
2158 Ok(self.submitted_frame(frame))
2159 }
2160
2161 pub fn submit_flat(
2162 &mut self,
2163 flat: &FlatScene<'_>,
2164 output: &wgpu::TextureView,
2165 ) -> Result<Submitted, String> {
2166 let frame = self.encode_flat_frame(flat, None, output)?;
2167 Ok(self.submitted_frame(frame))
2168 }
2169
2170 pub fn submit_flat_look(
2171 &mut self,
2172 flat: &FlatScene<'_>,
2173 look: &LookFrame<'_>,
2174 output: &wgpu::TextureView,
2175 ) -> Result<Submitted, String> {
2176 let frame = self.encode_flat_frame(flat, Some(look), output)?;
2177 Ok(self.submitted_frame(frame))
2178 }
2179
2180 pub fn flat_looks(&self) -> Option<&FlatLooks> {
2181 self.flat_looks.as_ref()
2182 }
2183
2184 fn submitted_frame(&mut self, frame: u64) -> Submitted {
2185 let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
2186 self.gather_timings(self.frames_submitted.saturating_sub(1));
2187 Submitted {
2188 frame,
2189 arrived: std::mem::take(&mut self.arrivals),
2190 }
2191 }
2192
2193 pub fn flat_pass(&self) -> Option<&FlatPass> {
2194 self.flat.as_ref()
2195 }
2196
2197 pub fn warm_flat<'a>(
2198 &mut self,
2199 format: wgpu::TextureFormat,
2200 size: [u32; 2],
2201 looks: impl IntoIterator<Item = &'a Look>,
2202 ) -> Result<(), String> {
2203 if size[0] == 0 || size[1] == 0 {
2204 return Err("warm_flat needs a size of at least 1 by 1".into());
2205 }
2206 let device = self.frame.gpu.device.clone();
2207 let queue = self.frame.gpu.queue.clone();
2208 let hdr = wgpu::TextureFormat::Rgba16Float;
2209 let finishes = [
2210 (format, true, false),
2211 (hdr, true, false),
2212 (hdr, self.display.is_some(), true),
2213 ];
2214 let mut flat = self
2215 .flat
2216 .take()
2217 .unwrap_or_else(|| FlatPass::new(&device, &queue));
2218 flat.warm(&device, size, &finishes, true);
2219 self.flat = Some(flat);
2220 let mut set = self
2221 .flat_looks
2222 .take()
2223 .unwrap_or_else(|| FlatLooks::new(&device, &queue));
2224 let warmed = set.warm(&device, &queue, looks, size);
2225 self.flat_looks = Some(set);
2226 warmed
2227 }
2228
2229 pub fn flat_pipelines_made_on_render_thread(&self) -> usize {
2230 self.flat.as_ref().map_or(0, FlatPass::pipelines_made)
2231 + self
2232 .flat_looks
2233 .as_ref()
2234 .map_or(0, FlatLooks::pipelines_made)
2235 }
2236
2237 fn encode_flat_frame(
2238 &mut self,
2239 scene: &FlatScene<'_>,
2240 look: Option<&LookFrame<'_>>,
2241 output: &wgpu::TextureView,
2242 ) -> Result<u64, String> {
2243 let look = look.filter(|look| !look.is_identity());
2244 if look.is_some() && self.size() != self.capacity() {
2245 return Err("a flat look renders at the full size; set the viewport to the renderer's capacity first".into());
2246 }
2247 self.begin_frame(FrameKind::Full);
2248 self.pass_log.clear();
2249 self.text_bounds.clear();
2250 let (width, height) = self.size();
2251 let (capacity_width, capacity_height) = self.capacity();
2252 let owned_device = self.frame.gpu.device.clone();
2253 let device = &owned_device;
2254 let queue = self.frame.gpu.queue.clone();
2255 if let Some(tape) = self.tape {
2256 self.post.set_tape(tape);
2257 }
2258 let mut flat = self
2259 .flat
2260 .take()
2261 .unwrap_or_else(|| FlatPass::new(device, &queue));
2262 flat.set_capacity(Some([capacity_width, capacity_height]));
2263 if look.is_none() {
2264 let prepared = flat.prepare(device, &queue, scene, [width, height]);
2265 if let Err(error) = prepared {
2266 self.flat = Some(flat);
2267 return Err(error);
2268 }
2269 }
2270 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2271 label: Some("live flat"),
2272 });
2273 let clear = Some(
2274 scene
2275 .clear
2276 .map_or([0.0; 4], |colour| colour.premultiplied()),
2277 );
2278 let direct = !scene.post && self.display.is_none();
2279 let target = if direct {
2280 output.clone()
2281 } else {
2282 flat.colour_target(device, [capacity_width, capacity_height])
2283 };
2284 if let Some(look) = look {
2285 let mut looks = self
2286 .flat_looks
2287 .take()
2288 .unwrap_or_else(|| FlatLooks::new(device, &queue));
2289 let encoded = looks.encode(
2290 device,
2291 &queue,
2292 &mut encoder,
2293 &mut flat,
2294 scene,
2295 look,
2296 [width, height],
2297 &target,
2298 Some(&mut self.profiler),
2299 );
2300 self.flat_looks = Some(looks);
2301 if let Err(error) = encoded {
2302 self.flat = Some(flat);
2303 return Err(error);
2304 }
2305 self.pass_log.push("flat look");
2306 } else {
2307 flat.encode_colour(&mut encoder, &target, clear, Some(&mut self.profiler));
2308 self.pass_log.push("flat");
2309 }
2310 if !direct {
2311 if scene.post {
2312 let hdr = &self.frame.targets.hdr.view;
2313 flat.encode_finish(
2314 device,
2315 &mut encoder,
2316 &target,
2317 hdr,
2318 wgpu::TextureFormat::Rgba16Float,
2319 true,
2320 false,
2321 Some(&mut self.profiler),
2322 );
2323 self.pass_log.push("flat finish");
2324 let post_output = self.display.as_ref().map_or(output, |(_, view)| view);
2325 self.post.run_timed_exposed(
2326 &mut encoder,
2327 hdr,
2328 None,
2329 None,
2330 post_output,
2331 scene.frame,
2332 scene.seed,
2333 1.0,
2334 Some(&mut self.profiler),
2335 );
2336 self.pass_log.push("post");
2337 if self.display.is_some() {
2338 let display = self.display.as_ref().map(|(_, view)| view.clone());
2339 if let Some(display) = display {
2340 self.encode_blit(device, &mut encoder, &display, output);
2341 }
2342 }
2343 } else {
2344 flat.encode_finish(
2345 device,
2346 &mut encoder,
2347 &target,
2348 output,
2349 self.output_format,
2350 true,
2351 false,
2352 Some(&mut self.profiler),
2353 );
2354 self.pass_log.push("flat finish");
2355 }
2356 }
2357 flat.encode_ids(
2358 &mut encoder,
2359 &self.frame.targets.ids_view,
2360 true,
2361 Some(&mut self.profiler),
2362 );
2363 self.pass_log.push("flat ids");
2364 self.flat = Some(flat);
2365 self.finish_encoder(device, &queue, encoder, false)
2366 }
2367
2368 pub fn frame_stats(&self) -> FrameStats {
2369 self.stats.clone()
2370 }
2371
2372 pub fn set_frame_stats(&mut self, stats: FrameStats) {
2373 self.stats = stats;
2374 }
2375
2376 fn begin_frame(&mut self, kind: FrameKind) {
2377 self.stats.begin(kind);
2378 self.resolution_frame = kind == FrameKind::Full;
2379 }
2380
2381 pub fn set_dynamic_resolution(&mut self, budget: Option<Budget>) -> Result<(), String> {
2382 self.resolution = budget.map(DynamicResolution::new).transpose()?;
2383 self.resolution_content = None;
2384 Ok(())
2385 }
2386
2387 pub fn dynamic_resolution(&self) -> Option<&DynamicResolution> {
2388 self.resolution.as_ref()
2389 }
2390
2391 pub fn apply_dynamic_resolution(&mut self, content: [u32; 2]) -> Result<(u32, u32), String> {
2392 let Some(resolution) = self.resolution.as_ref() else {
2393 return Ok(self.size());
2394 };
2395 let [width, height] = resolution
2396 .render_size(content)
2397 .ok_or("the content size must be at least 1 by 1")?;
2398 let [capacity_width, capacity_height] = resolution
2399 .capacity(content)
2400 .ok_or("the content size must be at least 1 by 1")?;
2401 self.resolution_content = Some(content);
2402 if self.capacity() != (capacity_width, capacity_height) {
2403 self.resize(capacity_width, capacity_height)?;
2404 }
2405 let (width, height) = (width.min(capacity_width), height.min(capacity_height));
2406 self.set_viewport(width, height)?;
2407 Ok((width, height))
2408 }
2409
2410 pub fn timings(&self) -> Option<&FrameTimings> {
2411 self.latest_timings.as_ref()
2412 }
2413
2414 pub fn timing_drops(&self) -> TimingDrops {
2415 TimingDrops {
2416 ring: self.profiler.dropped(),
2417 late: self.late_timings,
2418 }
2419 }
2420
2421 pub fn frames_submitted(&self) -> u64 {
2422 self.frames_submitted
2423 }
2424
2425 pub fn frames_in_flight(&self) -> u64 {
2426 self.frames_submitted
2427 .saturating_sub(self.frames_done.load(Ordering::Acquire))
2428 }
2429
2430 pub fn wait_frames(&mut self, in_flight: u64) -> Result<Vec<FrameTimings>, String> {
2431 let done = self.frames_done.load(Ordering::Acquire);
2432 while self
2433 .submissions
2434 .front()
2435 .is_some_and(|(frame, _)| *frame < done)
2436 {
2437 self.submissions.pop_front();
2438 }
2439 let waiting = self.submissions.len() as u64;
2440 if waiting > in_flight {
2441 let index = self.submissions[(waiting - in_flight - 1) as usize]
2442 .1
2443 .clone();
2444 self.frame
2445 .gpu
2446 .device
2447 .poll(wgpu::PollType::Wait {
2448 submission_index: Some(index),
2449 timeout: None,
2450 })
2451 .map_err(|error| format!("{error:?}"))?;
2452 } else {
2453 let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
2454 }
2455 self.gather_timings(self.frames_submitted.saturating_sub(1));
2456 Ok(std::mem::take(&mut self.arrivals))
2457 }
2458
2459 fn gather_timings(&mut self, current: u64) {
2460 for timings in self.profiler.gather() {
2461 if self.stats.enabled() || self.resolution.is_some() {
2462 let total = timings.passes.iter().map(|pass| pass.milliseconds).sum();
2463 self.stats.gpu(timings.frame, total);
2464 if let Some(resolution) = self.resolution.as_mut() {
2465 resolution.observe(timings.frame, total);
2466 }
2467 }
2468 if current.saturating_sub(timings.frame) > TIMING_LAG {
2469 self.late_timings += 1;
2470 continue;
2471 }
2472 let timings = FrameTimings {
2473 frame: timings.frame,
2474 passes: plain_passes(timings.passes),
2475 };
2476 if self
2477 .latest_timings
2478 .as_ref()
2479 .is_none_or(|latest| latest.frame < timings.frame)
2480 {
2481 self.latest_timings = Some(timings.clone());
2482 }
2483 self.arrivals.push(timings);
2484 }
2485 }
2486
2487 #[allow(clippy::too_many_arguments)]
2488 fn encode_frame(
2489 &mut self,
2490 scene: &Scene<'_>,
2491 text: &Text<'_>,
2492 effects: &Effects<'_>,
2493 style: Finish,
2494 hud: Option<&FlatScene<'_>>,
2495 output: &wgpu::TextureView,
2496 cached: bool,
2497 ) -> Result<u64, String> {
2498 if !cached {
2499 self.begin_frame(FrameKind::Full);
2500 }
2501 self.pass_log.clear();
2502 let (width, height) = self.size();
2503 self.text.set_viewport(Some([width, height]));
2504 self.text_bounds = text_bounds(text, [width, height]);
2505 if !scene.time.is_finite() || scene.time < 0.0 {
2506 return Err("scene time must be finite and nonnegative".into());
2507 }
2508 if let Some(shown) = self.frame.sky_mut().swap_settled() {
2509 self.sky_shown(shown)?;
2510 }
2511 let frame_number = (scene.time * 60.0).round() as u32;
2512 let view_projection = frame::multiply(scene.camera.projection, scene.camera.view);
2513 let mut camera = scene.camera;
2514 if cached {
2515 if let Some(last) = self.history.last_view_projection() {
2516 camera.previous_view_projection = last;
2517 }
2518 self.history.bridge(scene.time);
2519 }
2520 let valid =
2521 self.history
2522 .advance(scene.time, camera.previous_view_projection, view_projection);
2523 self.history_valid = valid;
2524 if !valid {
2525 self.taa.invalidate();
2526 }
2527 let metered = if matches!(self.exposure_mode, Exposure::Auto { .. }) {
2528 self.exposure.advance(scene.time, valid)
2529 } else {
2530 1.0
2531 };
2532 let exposure = self.exposure_mode.value(metered);
2533 let owned_device = self.frame.gpu.device.clone();
2534 let device = &owned_device;
2535 let queue = self.frame.gpu.queue.clone();
2536 if style != self.style {
2537 if !self.custom_finish {
2538 self.post = self.post_chain(chain(style, self.display.is_none()))?;
2539 }
2540 self.style = style;
2541 }
2542 if let Some(tape) = self.tape {
2543 self.post.set_tape(tape);
2544 }
2545 let mut lod_instances = Vec::new();
2546 if self.lod.instance_mut(0).is_some() {
2547 self.lod.select(scene.camera, height);
2548 let mut lod = std::mem::replace(&mut self.lod, LodSet::new(0));
2549 let uploaded = lod.upload_selected(&mut self.frame);
2550 self.lod = lod;
2551 lod_instances = uploaded?;
2552 self.pass_log.push("lod");
2553 }
2554 let dynamic = scene.instances;
2555 let stand_ins: &[Instance] = if self.plates.is_some() {
2556 &self.plate_casters
2557 } else {
2558 &[]
2559 };
2560 let combined: Vec<Instance>;
2561 let instances = if lod_instances.is_empty() && stand_ins.is_empty() {
2562 scene.instances
2563 } else {
2564 combined = lod_instances
2565 .into_iter()
2566 .chain(scene.instances.iter().copied())
2567 .chain(stand_ins.iter().copied())
2568 .collect();
2569 &combined
2570 };
2571 let scene = Scene {
2572 camera,
2573 time: scene.time,
2574 seed: scene.seed,
2575 sun: scene.sun,
2576 instances,
2577 materials: scene.materials,
2578 deformers: scene.deformers,
2579 wind: scene.wind,
2580 };
2581 let jitter = taa::jitter_clip(frame_number, width, height);
2582 self.frame.set_jitter(jitter);
2583 let projection = frame::multiply(frame::jitter_matrix(jitter), scene.camera.projection);
2584 let jittered = frame::multiply(projection, scene.camera.view);
2585 let view = camera_view(&scene, width, height);
2586 let right = [
2587 scene.camera.view[0][0],
2588 scene.camera.view[1][0],
2589 scene.camera.view[2][0],
2590 ];
2591 let lens = [
2592 1.0 / scene.camera.projection[0][0],
2593 1.0 / scene.camera.projection[1][1],
2594 ];
2595 let shift = [scene.camera.projection[2][0], scene.camera.projection[2][1]];
2596 let sky_forward = if shift == [0.0; 2] {
2597 view.forward
2598 } else {
2599 std::array::from_fn(|k| {
2600 view.forward[k] + right[k] * lens[0] * shift[0] + view.up[k] * lens[1] * shift[1]
2601 })
2602 };
2603 self.frame.sky_mut().update(
2604 &queue,
2605 sky_forward,
2606 right.map(|value| value * lens[0]),
2607 view.up.map(|value| value * lens[1]),
2608 );
2609 self.frame
2610 .set_reflections(self.reflections.result().filter(|_| valid));
2611 let fit = self.shadows.fit(&view, scene.sun.direction);
2612 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2613 label: Some("live renderer"),
2614 });
2615 let shadow_props = self
2616 .impostors
2617 .as_ref()
2618 .filter(|_| !effects.props.is_empty());
2619 let cascades: Vec<Matrix> = fit
2620 .cascades
2621 .iter()
2622 .map(|cascade| cascade.view_proj.columns)
2623 .collect();
2624 let (fluid, ripple) = effects
2625 .liquid
2626 .as_ref()
2627 .map_or((&self.black, &self.black), |liquid| {
2628 (liquid.fluid, liquid.ripple)
2629 });
2630 let faces = self.frame.lights().faces();
2631 let light_faces = faces.len();
2632 let cast_layers: Vec<Matrix> = cascades
2633 .iter()
2634 .copied()
2635 .chain(faces.into_iter().map(|(_, matrix)| matrix))
2636 .collect();
2637 let glass_casting =
2638 self.glass
2639 .prepare_casts(device, &queue, &cast_layers, effects.glass, fluid, ripple)?;
2640 let glass = &self.glass;
2641 let mut glass_casts = |pass: &mut wgpu::RenderPass<'_>, layer: usize, _: bool| {
2642 glass.draw_casts(pass, layer);
2643 };
2644 self.frame.encode_with_casts(
2645 &scene,
2646 &mut encoder,
2647 Some((&mut self.shadows, &fit)),
2648 Some(&mut self.profiler),
2649 if glass_casting {
2650 Some(&mut glass_casts)
2651 } else {
2652 None
2653 },
2654 |gpu, encoder, shadows, fit| {
2655 if let Some(props) = shadow_props {
2656 props.update_shadows(gpu, fit, self.hour, effects.props)?;
2657 shadows.render_impostors(encoder, props, effects.props.len())?;
2658 }
2659 if let Some(mesh) = effects.creature_mesh
2660 && !effects.creatures.is_empty()
2661 {
2662 shadows.render_creatures(
2663 gpu,
2664 encoder,
2665 fit,
2666 mesh,
2667 effects.creatures,
2668 scene.time,
2669 );
2670 }
2671 Ok(())
2672 },
2673 )?;
2674 if self.frame.has_tree() {
2675 self.pass_log.push("canopy cookie");
2676 }
2677 self.pass_log.push("shadows");
2678 if self.frame.lights().any_transmissive() || glass_casting {
2679 self.pass_log.push("shadow transmission");
2680 }
2681 if glass_casting && light_faces > 0 {
2682 self.pass_log.push("light transmission");
2683 }
2684 if shadow_props.is_some() {
2685 self.pass_log.push("impostor shadows");
2686 }
2687 if effects.creature_mesh.is_some() && !effects.creatures.is_empty() {
2688 self.pass_log.push("creature shadows");
2689 }
2690 if !self.frame.lights().shadowed().is_empty() {
2691 self.pass_log.push("light shadows");
2692 }
2693 self.pass_log.extend(["prepass", "opaque"]);
2694 if let Some(props) = &self.impostors
2695 && !effects.props.is_empty()
2696 {
2697 props.update(
2698 &self.frame.gpu,
2699 ImpostorParams {
2700 view_projection: jittered,
2701 camera_position: [
2702 scene.camera.position[0],
2703 scene.camera.position[1],
2704 scene.camera.position[2],
2705 0.0,
2706 ],
2707 camera_right: [right[0], right[1], right[2], 0.0],
2708 camera_up: [view.up[0], view.up[1], view.up[2], 0.0],
2709 settings: [0.0; 4],
2710 source: [0.0; 4],
2711 },
2712 self.hour,
2713 effects.props,
2714 )?;
2715 let timing = self.profiler.pass("impostors");
2716 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2717 multiview_mask: None,
2718 label: Some("impostors"),
2719 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2720 view: &self.frame.targets.hdr.view,
2721 resolve_target: None,
2722 depth_slice: None,
2723 ops: wgpu::Operations {
2724 load: wgpu::LoadOp::Load,
2725 store: wgpu::StoreOp::Store,
2726 },
2727 })],
2728 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2729 view: &self.frame.targets.depth_view,
2730 depth_ops: Some(wgpu::Operations {
2731 load: wgpu::LoadOp::Load,
2732 store: wgpu::StoreOp::Store,
2733 }),
2734 stencil_ops: None,
2735 }),
2736 timestamp_writes: self.profiler.render_writes(timing),
2737 occlusion_query_set: None,
2738 });
2739 crate::viewport::apply(&mut pass, [width, height]);
2740 props.draw(&mut pass, effects.props.len())?;
2741 drop(pass);
2742 self.pass_log.push("impostors");
2743 }
2744 if let Some(plates) = self.plates.as_mut() {
2745 let shadowed = plates.encode(
2746 &self.frame,
2747 (&self.shadows, &fit),
2748 &self.reactive_view,
2749 &mut encoder,
2750 &crate::plates::PlateFrame {
2751 view_projection,
2752 jittered,
2753 previous: scene.camera.previous_view_projection,
2754 view: scene.camera.view,
2755 eye: scene.camera.position,
2756 toward_sun: scene.sun.direction,
2757 sun_light: scene
2758 .sun
2759 .colour
2760 .map(|channel| channel * scene.sun.intensity),
2761 sun_up: scene.sun.intensity > 0.0,
2762 instances: dynamic,
2763 },
2764 Some(&mut self.profiler),
2765 )?;
2766 if shadowed {
2767 self.pass_log.push("plate shadows");
2768 }
2769 self.pass_log.push("plate composite");
2770 self.plate_mask = true;
2771 } else if self.plate_mask {
2772 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2773 multiview_mask: None,
2774 label: Some("plate mask clear"),
2775 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2776 view: &self.reactive_view,
2777 resolve_target: None,
2778 depth_slice: None,
2779 ops: wgpu::Operations {
2780 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2781 store: wgpu::StoreOp::Store,
2782 },
2783 })],
2784 depth_stencil_attachment: None,
2785 timestamp_writes: None,
2786 occlusion_query_set: None,
2787 });
2788 self.plate_mask = false;
2789 }
2790 self.depth_index = 1 - self.depth_index;
2791 let current_depth = &self.depths[self.depth_index].1;
2792 let depth_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2793 label: Some("live depth limits"),
2794 contents: bytemuck::cast_slice(&[
2795 scene.camera.projection[2][2],
2796 scene.camera.projection[3][2],
2797 view.far,
2798 0.0,
2799 ]),
2800 usage: wgpu::BufferUsages::UNIFORM,
2801 });
2802 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2803 label: Some("live linear depth"),
2804 layout: &self.depth_layout,
2805 entries: &[
2806 wgpu::BindGroupEntry {
2807 binding: 0,
2808 resource: wgpu::BindingResource::TextureView(&self.frame.targets.depth_view),
2809 },
2810 wgpu::BindGroupEntry {
2811 binding: 1,
2812 resource: wgpu::BindingResource::TextureView(current_depth),
2813 },
2814 wgpu::BindGroupEntry {
2815 binding: 2,
2816 resource: depth_uniform.as_entire_binding(),
2817 },
2818 ],
2819 });
2820 let timing = self.profiler.pass("linear depth");
2821 {
2822 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2823 label: Some("linear depth"),
2824 timestamp_writes: self.profiler.compute_writes(timing),
2825 });
2826 pass.set_pipeline(&self.depth_pipeline);
2827 pass.set_bind_group(0, &bind, &[]);
2828 pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
2829 }
2830 self.pass_log.push("linear depth");
2831 let inverse = invert(jittered).ok_or("camera projection is singular")?;
2832 let mut motion = Vec::with_capacity(52);
2833 for matrix in [
2834 inverse,
2835 view_projection,
2836 scene.camera.previous_view_projection,
2837 ] {
2838 motion.extend(matrix.iter().flatten().copied());
2839 }
2840 motion.extend([
2841 width as f32,
2842 height as f32,
2843 1.0 / width as f32,
2844 1.0 / height as f32,
2845 ]);
2846 queue.write_buffer(&self.motion.uniform, 0, bytemuck::cast_slice(&motion));
2847 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2848 label: Some("live background motion"),
2849 layout: &self.motion.layout,
2850 entries: &[
2851 wgpu::BindGroupEntry {
2852 binding: 0,
2853 resource: wgpu::BindingResource::TextureView(&self.frame.targets.depth_view),
2854 },
2855 wgpu::BindGroupEntry {
2856 binding: 1,
2857 resource: wgpu::BindingResource::TextureView(&self.frame.targets.ids_view),
2858 },
2859 wgpu::BindGroupEntry {
2860 binding: 2,
2861 resource: self.motion.uniform.as_entire_binding(),
2862 },
2863 ],
2864 });
2865 let timing = self.profiler.pass("background motion");
2866 {
2867 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2868 multiview_mask: None,
2869 label: Some("background motion"),
2870 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2871 view: &self.frame.targets.velocity_view,
2872 resolve_target: None,
2873 depth_slice: None,
2874 ops: wgpu::Operations {
2875 load: wgpu::LoadOp::Load,
2876 store: wgpu::StoreOp::Store,
2877 },
2878 })],
2879 depth_stencil_attachment: None,
2880 timestamp_writes: self.profiler.render_writes(timing),
2881 occlusion_query_set: None,
2882 });
2883 crate::viewport::apply(&mut pass, [width, height]);
2884 pass.set_pipeline(&self.motion.pipeline);
2885 pass.set_bind_group(0, &bind, &[]);
2886 pass.draw(0..3, 0..1);
2887 }
2888 self.pass_log.push("background motion");
2889 let timing = self.profiler.pass("sky");
2890 {
2891 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2892 multiview_mask: None,
2893 label: Some("sky"),
2894 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2895 view: &self.frame.targets.hdr.view,
2896 resolve_target: None,
2897 depth_slice: None,
2898 ops: wgpu::Operations {
2899 load: wgpu::LoadOp::Load,
2900 store: wgpu::StoreOp::Store,
2901 },
2902 })],
2903 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2904 view: &self.frame.targets.depth_view,
2905 depth_ops: Some(wgpu::Operations {
2906 load: wgpu::LoadOp::Load,
2907 store: wgpu::StoreOp::Store,
2908 }),
2909 stencil_ops: None,
2910 }),
2911 timestamp_writes: self.profiler.render_writes(timing),
2912 occlusion_query_set: None,
2913 });
2914 crate::viewport::apply(&mut pass, [width, height]);
2915 self.frame.sky().draw(&mut pass);
2916 }
2917 self.pass_log.push("sky");
2918 self.mips.copy_scene(
2919 &mut encoder,
2920 &self.frame.targets.hdr.texture,
2921 [width, height],
2922 );
2923 let timing = self.profiler.pass("scene colour mips");
2924 let fresh = self.mips.written != Some([width, height]);
2925 self.mips.written = Some([width, height]);
2926 for level in 1..self.mips.levels.len() {
2927 let rect = [width, height].map(|size| crate::viewport::mip(size, level as u32));
2928 if fresh {
2929 queue.write_buffer(
2930 &self.mips.extents[level],
2931 0,
2932 bytemuck::cast_slice(&[rect[0], rect[1], 0, 0]),
2933 );
2934 }
2935 let last = level + 1 == self.mips.levels.len();
2936 let writes = if level == 1 && last {
2937 self.profiler.render_writes(timing)
2938 } else if level == 1 {
2939 self.profiler
2940 .render_writes(timing)
2941 .map(|writes| wgpu::RenderPassTimestampWrites {
2942 end_of_pass_write_index: None,
2943 ..writes
2944 })
2945 } else if last {
2946 self.profiler
2947 .render_writes(timing)
2948 .map(|writes| wgpu::RenderPassTimestampWrites {
2949 beginning_of_pass_write_index: None,
2950 ..writes
2951 })
2952 } else {
2953 None
2954 };
2955 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2956 multiview_mask: None,
2957 label: Some("scene colour mip"),
2958 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2959 view: &self.mips.levels[level],
2960 resolve_target: None,
2961 depth_slice: None,
2962 ops: wgpu::Operations {
2963 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2964 store: wgpu::StoreOp::Store,
2965 },
2966 })],
2967 depth_stencil_attachment: None,
2968 timestamp_writes: writes,
2969 occlusion_query_set: None,
2970 });
2971 crate::viewport::apply(&mut pass, self.mips.bordered(rect, level as u32));
2972 pass.set_pipeline(&self.mips.pipeline);
2973 pass.set_bind_group(0, &self.mips.binds[level - 1], &[]);
2974 pass.draw(0..3, 0..1);
2975 }
2976 self.pass_log.push("scene colour");
2977 let inverse_projection = invert(projection).ok_or("camera projection is singular")?;
2978 self.reflections.encode(
2979 device,
2980 &mut encoder,
2981 &reflect::Inputs {
2982 depth: current_depth,
2983 normal_roughness: &self.frame.targets.normal_roughness_view,
2984 colour: &self.mips.view,
2985 motion: &self.frame.targets.velocity_view,
2986 projection,
2987 inverse_projection,
2988 frame_index: frame_number,
2989 thickness: reflect::THICKNESS,
2990 history_valid: valid,
2991 },
2992 Some(&mut self.profiler),
2993 );
2994 self.pass_log.push("reflections");
2995 let mut reactive = &self.reactive_view;
2996 if glass::drawn(effects.glass) {
2997 let cube = self.frame.sky().cube_view();
2998 let caustic = self.frame.caustic();
2999 let content = self.frame.content();
3000 let caustic_view = caustic
3001 .as_ref()
3002 .map_or(&self.black, |caustic| content.view(caustic.slot));
3003 let (fluid, ripple) = effects
3004 .liquid
3005 .as_ref()
3006 .map_or((&self.black, &self.black), |liquid| {
3007 (liquid.fluid, liquid.ripple)
3008 });
3009 self.glass.encode(
3010 device,
3011 &queue,
3012 &mut encoder,
3013 glass::Render {
3014 camera: glass::Camera {
3015 view: scene.camera.view,
3016 projection,
3017 },
3018 surfaces: effects.glass,
3019 inputs: glass::Inputs {
3020 opaque_hdr_mips: &self.mips.view,
3021 opaque_depth: &self.frame.targets.depth_view,
3022 reflection_cube: &cube,
3023 caustic: caustic_view,
3024 caustic_projection: caustic
3025 .as_ref()
3026 .map(|caustic| caustic.projection(scene.sun.direction)),
3027 fluid_height: fluid,
3028 ripple_height: ripple,
3029 mip_count: self.mips.levels.len() as u32,
3030 sky_sh: self.frame.sky().lighting.sh,
3031 },
3032 output: &self.frame.targets.hdr.view,
3033 profiler: Some(&mut self.profiler),
3034 },
3035 )?;
3036 reactive = self.glass.reactive();
3037 self.pass_log.push("glass");
3038 }
3039 let mut effect_frame = EffectFrame::new(width, height, scene.time, scene.seed);
3040 effect_frame.view_proj = jittered;
3041 effect_frame.eye = [
3042 scene.camera.position[0],
3043 scene.camera.position[1],
3044 scene.camera.position[2],
3045 0.0,
3046 ];
3047 effect_frame.right = [right[0], right[1], right[2], 0.0];
3048 effect_frame.up = [view.up[0], view.up[1], view.up[2], 0.0];
3049 effect_frame.forward = [view.forward[0], view.forward[1], view.forward[2], 0.0];
3050 effect_frame.lens = [lens[0], lens[1], 0.0, 0.0];
3051 effect_frame.sun = [
3052 scene.sun.direction[0],
3053 scene.sun.direction[1],
3054 scene.sun.direction[2],
3055 0.0,
3056 ];
3057 effect_frame.sun_color = [
3058 scene.sun.colour[0],
3059 scene.sun.colour[1],
3060 scene.sun.colour[2],
3061 scene.sun.intensity,
3062 ];
3063 let shadow_data = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3064 label: Some("effects sun cascades"),
3065 contents: &self.shadows.sample_uniform(&fit),
3066 usage: wgpu::BufferUsages::UNIFORM,
3067 });
3068 let targets = EffectTargets {
3069 hdr: &self.frame.targets.hdr.view,
3070 depth: current_depth,
3071 shadow: Some((self.shadows.atlas_view(), &shadow_data)),
3072 };
3073 if let Some(haze) = self.haze {
3074 let mut room_frame = effect_frame;
3075 room_frame.set_room_haze(haze);
3076 self.effects.prepare_room(
3077 &mut EffectCommands {
3078 device,
3079 encoder: &mut encoder,
3080 },
3081 &haze,
3082 );
3083 let march = self.profiler.pass("haze march");
3084 let upsample = self.profiler.pass("haze upsample");
3085 self.effects.render_volume(
3086 &mut EffectCommands {
3087 device,
3088 encoder: &mut encoder,
3089 },
3090 &room_frame,
3091 targets,
3092 VolumeTimestamps {
3093 march: self.profiler.render_writes(march),
3094 upsample: self.profiler.render_writes(upsample),
3095 },
3096 );
3097 self.pass_log.extend(["haze march", "haze upsample"]);
3098 }
3099 if let Some(volume) = effects.volume.as_ref().filter(|_| !cached) {
3100 let mut volume_frame = effect_frame;
3101 volume_frame.set_volume(
3102 volume.kind,
3103 volume.lo,
3104 volume.hi,
3105 volume.color,
3106 volume.density,
3107 volume.anisotropy,
3108 );
3109 volume_frame.scale_time(volume.time_scale);
3110 let march = self.profiler.pass("volume march");
3111 let upsample = self.profiler.pass("volume upsample");
3112 self.effects.render_volume(
3113 &mut EffectCommands {
3114 device,
3115 encoder: &mut encoder,
3116 },
3117 &volume_frame,
3118 targets,
3119 VolumeTimestamps {
3120 march: self.profiler.render_writes(march),
3121 upsample: self.profiler.render_writes(upsample),
3122 },
3123 );
3124 self.pass_log.extend(["volume march", "volume upsample"]);
3125 }
3126 let petals = self
3127 .petals
3128 .as_mut()
3129 .map(|petals| petals.step(scene.time, &scene.wind.current))
3130 .unwrap_or_default();
3131 let own: &[ParticleInstance] = if cached { &[] } else { effects.particles };
3132 let particles: Vec<ParticleInstance>;
3133 let particles = if petals.is_empty() {
3134 own
3135 } else {
3136 particles = own.iter().copied().chain(petals).collect();
3137 &particles
3138 };
3139 if !particles.is_empty() {
3140 let timing = self.profiler.pass("particles");
3141 self.effects.render_particles(
3142 &mut EffectCommands {
3143 device,
3144 encoder: &mut encoder,
3145 },
3146 &effect_frame,
3147 targets,
3148 particles,
3149 self.profiler.render_writes(timing),
3150 );
3151 self.pass_log.push("particles");
3152 }
3153 if let Some(mesh) = effects.creature_mesh
3154 && !effects.creatures.is_empty()
3155 {
3156 let timing = self.profiler.pass("creatures");
3157 self.effects.render_creatures(
3158 &mut EffectCommands {
3159 device,
3160 encoder: &mut encoder,
3161 },
3162 &effect_frame,
3163 targets,
3164 mesh,
3165 effects.creatures,
3166 self.profiler.render_writes(timing),
3167 );
3168 self.pass_log.push("creatures");
3169 }
3170 let deformers = self.text.deformer_group(
3171 device,
3172 self.frame.deformer_buffer().buffer(),
3173 self.frame.deformer_buffer().plan_buffer(),
3174 );
3175 let surface_text = text
3176 .surface
3177 .iter()
3178 .any(|item| !item.paragraph.glyphs.is_empty())
3179 || text.surface_quads.iter().any(|item| !item.quads.is_empty());
3180 if surface_text && !self.text_after_taa {
3181 draw_surface_text(
3182 &mut self.text,
3183 &self.frame,
3184 &mut self.profiler,
3185 device,
3186 &mut encoder,
3187 text,
3188 &deformers,
3189 &self.frame.targets.hdr.view,
3190 )?;
3191 self.pass_log.push("surface text");
3192 }
3193 self.taa.resolve_timed(
3194 device,
3195 &queue,
3196 &mut encoder,
3197 &GpuInputs {
3198 colour: &self.frame.targets.hdr.view,
3199 depth: current_depth,
3200 motion: &self.frame.targets.velocity_view,
3201 reactive,
3202 id: Some(&self.frame.targets.ids_view),
3203 normal: Some(&self.frame.targets.normal_roughness_view),
3204 exposure,
3205 },
3206 &taa::Settings::default(),
3207 Some(&mut self.profiler),
3208 )?;
3209 self.pass_log.push("TAA");
3210 let resolved = self
3211 .taa
3212 .history_view()
3213 .ok_or("TAA output is empty")?
3214 .clone();
3215 let late_text = surface_text && self.text_after_taa;
3216 let resolved = if self.sharpen > 0.0 || late_text {
3217 let display = self.crisp.encode(
3218 device,
3219 &queue,
3220 &mut encoder,
3221 &resolved,
3222 self.sharpen,
3223 exposure,
3224 Some(&mut self.profiler),
3225 );
3226 self.pass_log.push(if self.sharpen > 0.0 {
3227 "sharpen"
3228 } else {
3229 "crisp copy"
3230 });
3231 if late_text {
3232 draw_surface_text(
3233 &mut self.text,
3234 &self.frame,
3235 &mut self.profiler,
3236 device,
3237 &mut encoder,
3238 text,
3239 &deformers,
3240 display,
3241 )?;
3242 self.pass_log.push("surface text");
3243 }
3244 display.clone()
3245 } else {
3246 resolved
3247 };
3248 let layered = cached && (effects.volume.is_some() || !effects.particles.is_empty());
3249 let resolved = if let Some(cache) = self.demand.as_ref().filter(|_| cached) {
3250 let whole = [Region::whole(width, height)];
3251 if layered {
3252 cache.encode_copy(
3253 device,
3254 &mut encoder,
3255 &resolved,
3256 &cache.still.view,
3257 &whole,
3258 "demand still",
3259 &mut self.profiler,
3260 );
3261 self.pass_log.push("demand still");
3262 let layer = cache.layer.view.clone();
3263 cache.encode_copy(
3264 device,
3265 &mut encoder,
3266 &resolved,
3267 &layer,
3268 &whole,
3269 "demand layer",
3270 &mut self.profiler,
3271 );
3272 let mut layer_frame = effect_frame;
3273 layer_frame.view_proj = view_projection;
3274 draw_layers(
3275 device,
3276 &mut encoder,
3277 &mut self.effects,
3278 &mut self.profiler,
3279 effects,
3280 &layer_frame,
3281 EffectTargets {
3282 hdr: &layer,
3283 depth: current_depth,
3284 shadow: Some((self.shadows.atlas_view(), &shadow_data)),
3285 },
3286 &mut self.pass_log,
3287 );
3288 layer
3289 } else {
3290 resolved
3291 }
3292 } else {
3293 resolved
3294 };
3295 let resolved = if let Some(lens) = self.lens {
3296 let timing = self.profiler.pass("depth of field");
3297 self.pass_log.push("depth of field");
3298 self.dof.render(
3299 &mut encoder,
3300 &resolved,
3301 current_depth,
3302 lens,
3303 dof::height_plane(scene.camera.view, scene.camera.projection),
3304 self.profiler.compute_writes(timing),
3305 )
3306 } else {
3307 &resolved
3308 };
3309 if matches!(self.exposure_mode, Exposure::Auto { .. }) {
3310 self.exposure
3311 .encode(device, &queue, &mut encoder, resolved, [width, height]);
3312 }
3313 let overlays = !text.overlay.is_empty() || !text.overlay_quads.is_empty() || hud.is_some();
3314 let colour = self
3315 .demand
3316 .as_ref()
3317 .filter(|_| cached)
3318 .map(|cache| cache.colour.view.clone());
3319 let post_output = match &colour {
3320 Some(view) => view,
3321 None => self.display.as_ref().map_or(output, |(_, view)| view),
3322 };
3323 self.post.run_timed_exposed(
3324 &mut encoder,
3325 resolved,
3326 Some(&self.frame.targets.depth_view),
3327 Some(&self.frame.targets.normal_roughness_view),
3328 post_output,
3329 frame_number,
3330 scene.seed,
3331 exposure,
3332 Some(&mut self.profiler),
3333 );
3334 self.pass_log.push("post");
3335 if let Some(cache) = self.demand.as_mut().filter(|_| cached) {
3336 cache.probe_depth(&mut encoder, &self.depths[self.depth_index].0);
3337 cache.encode_tiles(
3338 device,
3339 &mut encoder,
3340 &self.depths[self.depth_index].1,
3341 &mut self.profiler,
3342 );
3343 self.pass_log.push("demand tiles");
3344 let previous = cache.cached.as_ref().map(|cached| cached.exposure);
3345 cache.exposure_moving = previous
3346 .is_some_and(|previous| (previous - exposure).abs() > 1e-3 * exposure.abs());
3347 cache.cached = Some(demand::Cached {
3348 view: scene.camera.view,
3349 projection: scene.camera.projection,
3350 far: view.far,
3351 depth_index: self.depth_index,
3352 shadow: self.shadows.sample_uniform(&fit).to_vec(),
3353 frame_number,
3354 exposure,
3355 });
3356 cache.colour_layers = layered;
3357 cache.book.still = layered;
3358 cache.last = demand::Last::Colour;
3359 cache.overlaid = overlays;
3360 cache.overlay_print = None;
3361 if overlays {
3362 cache.encode_clear(&mut encoder);
3363 }
3364 }
3365 let overlay_target = match self.demand.as_ref().filter(|_| cached) {
3366 Some(cache) => cache.overlay.view.clone(),
3367 None => post_output.clone(),
3368 };
3369 for item in &text.overlay {
3370 self.text
3371 .encode_timed(
3372 device,
3373 &mut encoder,
3374 TextDraw {
3375 target: &overlay_target,
3376 depth: None,
3377 atlas: item.atlas,
3378 paragraph: item.paragraph,
3379 space: item.space,
3380 color: item.color,
3381 deformers: None,
3382 },
3383 Some(&mut self.profiler),
3384 )
3385 .map_err(str::to_owned)?;
3386 }
3387 for item in &text.overlay_quads {
3388 self.text
3389 .encode_quads_timed(
3390 device,
3391 &mut encoder,
3392 QuadDraw {
3393 target: &overlay_target,
3394 depth: None,
3395 atlas: item.atlas,
3396 quads: item.quads,
3397 space: item.space,
3398 deformers: None,
3399 icons: item.icons,
3400 },
3401 Some(&mut self.profiler),
3402 )
3403 .map_err(str::to_owned)?;
3404 }
3405 if !text.overlay.is_empty() || !text.overlay_quads.is_empty() {
3406 self.pass_log.push("overlay text");
3407 }
3408 let ids = &self.frame.targets.ids_view;
3409 if glass::drawn(effects.glass) && self.glass.has_ids() {
3410 self.glass
3411 .encode_ids(&mut encoder, ids, Some(&mut self.profiler));
3412 self.pass_log.push("glass ids");
3413 }
3414 let lit = self.frame.lit_bindings();
3415 let lit_for = |space: &TextSpace| {
3416 if space.is_lit() {
3417 lit.as_ref()
3418 .map(Some)
3419 .ok_or("frame lighting is not prepared")
3420 } else {
3421 Ok(None)
3422 }
3423 };
3424 for item in text.surface.iter().filter(|item| item.id != 0) {
3425 self.text
3426 .encode_ids(
3427 device,
3428 &mut encoder,
3429 TextDraw {
3430 target: ids,
3431 depth: Some(&self.frame.targets.depth_view),
3432 atlas: item.atlas,
3433 paragraph: item.paragraph,
3434 space: item.space,
3435 color: item.color,
3436 deformers: Some(&deformers),
3437 },
3438 lit_for(&item.space)?,
3439 item.id,
3440 Some(&mut self.profiler),
3441 )
3442 .map_err(str::to_owned)?;
3443 }
3444 for item in text.surface_quads.iter().filter(|item| item.id != 0) {
3445 self.text
3446 .encode_quad_ids(
3447 device,
3448 &mut encoder,
3449 QuadDraw {
3450 target: ids,
3451 depth: Some(&self.frame.targets.depth_view),
3452 atlas: item.atlas,
3453 quads: item.quads,
3454 space: item.space,
3455 deformers: Some(&deformers),
3456 icons: item.icons,
3457 },
3458 lit_for(&item.space)?,
3459 item.id,
3460 Some(&mut self.profiler),
3461 )
3462 .map_err(str::to_owned)?;
3463 }
3464 for item in text.overlay.iter().filter(|item| item.id != 0) {
3465 self.text
3466 .encode_ids(
3467 device,
3468 &mut encoder,
3469 TextDraw {
3470 target: ids,
3471 depth: None,
3472 atlas: item.atlas,
3473 paragraph: item.paragraph,
3474 space: item.space,
3475 color: item.color,
3476 deformers: None,
3477 },
3478 None,
3479 item.id,
3480 Some(&mut self.profiler),
3481 )
3482 .map_err(str::to_owned)?;
3483 }
3484 for item in text.overlay_quads.iter().filter(|item| item.id != 0) {
3485 self.text
3486 .encode_quad_ids(
3487 device,
3488 &mut encoder,
3489 QuadDraw {
3490 target: ids,
3491 depth: None,
3492 atlas: item.atlas,
3493 quads: item.quads,
3494 space: item.space,
3495 deformers: None,
3496 icons: item.icons,
3497 },
3498 None,
3499 item.id,
3500 Some(&mut self.profiler),
3501 )
3502 .map_err(str::to_owned)?;
3503 }
3504 if text.surface.iter().any(|item| item.id != 0)
3505 || text.surface_quads.iter().any(|item| item.id != 0)
3506 || text.overlay.iter().any(|item| item.id != 0)
3507 || text.overlay_quads.iter().any(|item| item.id != 0)
3508 {
3509 self.pass_log.push("text ids");
3510 }
3511 if let Some(hud) = hud {
3512 let mut flat = self
3513 .flat
3514 .take()
3515 .unwrap_or_else(|| FlatPass::new(device, &queue));
3516 let (capacity_width, capacity_height) = self.capacity();
3517 flat.set_capacity(Some([capacity_width, capacity_height]));
3518 let prepared = flat.prepare(device, &queue, hud, [width, height]);
3519 if let Err(error) = prepared {
3520 self.flat = Some(flat);
3521 return Err(error);
3522 }
3523 let target = flat.colour_target(device, [capacity_width, capacity_height]);
3524 flat.encode_colour(
3525 &mut encoder,
3526 &target,
3527 Some([0.0; 4]),
3528 Some(&mut self.profiler),
3529 );
3530 flat.encode_finish(
3531 device,
3532 &mut encoder,
3533 &target,
3534 &overlay_target,
3535 wgpu::TextureFormat::Rgba16Float,
3536 self.display.is_some(),
3537 true,
3538 Some(&mut self.profiler),
3539 );
3540 flat.encode_ids(&mut encoder, ids, false, Some(&mut self.profiler));
3541 self.flat = Some(flat);
3542 self.pass_log.extend(["flat", "flat finish", "flat ids"]);
3543 }
3544 let source = post_output.clone();
3545 if cached && overlays {
3546 if let Some(cache) = self.demand.as_mut() {
3547 cache.encode_composite(
3548 device,
3549 &mut encoder,
3550 &source,
3551 (output, self.output_format),
3552 &mut self.profiler,
3553 );
3554 self.pass_log.push("composite");
3555 }
3556 } else if self.blit.is_some() {
3557 self.encode_blit(device, &mut encoder, &source, output);
3558 }
3559 self.finish_encoder(device, &queue, encoder, true)
3560 }
3561
3562 fn encode_blit(
3563 &mut self,
3564 device: &wgpu::Device,
3565 encoder: &mut wgpu::CommandEncoder,
3566 post_output: &wgpu::TextureView,
3567 output: &wgpu::TextureView,
3568 ) {
3569 if let Some((layout, pipeline)) = &self.blit {
3570 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3571 label: Some("live output blit"),
3572 layout,
3573 entries: &[wgpu::BindGroupEntry {
3574 binding: 0,
3575 resource: wgpu::BindingResource::TextureView(post_output),
3576 }],
3577 });
3578 let timing = self.profiler.pass("output blit");
3579 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3580 multiview_mask: None,
3581 label: Some("live output blit"),
3582 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3583 view: output,
3584 resolve_target: None,
3585 depth_slice: None,
3586 ops: wgpu::Operations {
3587 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3588 store: wgpu::StoreOp::Store,
3589 },
3590 })],
3591 depth_stencil_attachment: None,
3592 timestamp_writes: self.profiler.render_writes(timing),
3593 occlusion_query_set: None,
3594 });
3595 let [width, height] = self.frame.viewport();
3596 crate::viewport::apply(&mut pass, [width, height]);
3597 pass.set_pipeline(pipeline);
3598 pass.set_bind_group(0, &bind, &[]);
3599 pass.draw(0..3, 0..1);
3600 self.pass_log.push("output blit");
3601 }
3602 }
3603
3604 fn finish_encoder(
3605 &mut self,
3606 device: &wgpu::Device,
3607 queue: &wgpu::Queue,
3608 mut encoder: wgpu::CommandEncoder,
3609 depth: bool,
3610 ) -> Result<u64, String> {
3611 let pick_readback = if self.pick_readback.is_none() {
3612 self.pick_request.take().map(|(x, y)| {
3613 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3614 label: Some("live pick readback"),
3615 size: 512,
3616 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3617 mapped_at_creation: false,
3618 });
3619 for (texture, offset) in [
3620 (&self.frame.targets.ids, 0),
3621 (&self.depths[self.depth_index].0, 256),
3622 ]
3623 .into_iter()
3624 .take(if depth { 2 } else { 1 })
3625 {
3626 encoder.copy_texture_to_buffer(
3627 wgpu::TexelCopyTextureInfo {
3628 texture,
3629 mip_level: 0,
3630 origin: wgpu::Origin3d { x, y, z: 0 },
3631 aspect: wgpu::TextureAspect::All,
3632 },
3633 wgpu::TexelCopyBufferInfo {
3634 buffer: &buffer,
3635 layout: wgpu::TexelCopyBufferLayout {
3636 offset,
3637 bytes_per_row: Some(256),
3638 rows_per_image: Some(1),
3639 },
3640 },
3641 wgpu::Extent3d {
3642 width: 1,
3643 height: 1,
3644 depth_or_array_layers: 1,
3645 },
3646 );
3647 }
3648 (buffer, x, y)
3649 })
3650 } else {
3651 None
3652 };
3653 let number = self.frames_submitted;
3654 let _ = device.poll(wgpu::PollType::Poll);
3655 self.gather_timings(number);
3656 let slot = self.profiler.finish_frame(&mut encoder, number);
3657 let index = queue.submit(Some(encoder.finish()));
3658 let (width, height) = self.size();
3659 self.stats.submitted(number, [width, height]);
3660 if std::mem::take(&mut self.resolution_frame)
3661 && let Some(resolution) = self.resolution.as_mut()
3662 {
3663 let rendered = self
3664 .resolution_content
3665 .map_or(resolution.scale(), |content| {
3666 let area = f64::from(width) * f64::from(height);
3667 let full = f64::from(content[0]) * f64::from(content[1]);
3668 (area / full).sqrt() as f32
3669 });
3670 resolution.begin_at(number, rendered);
3671 }
3672 self.frames_submitted += 1;
3673 let done = self.frames_done.clone();
3674 queue.on_submitted_work_done(move || {
3675 done.fetch_max(number + 1, Ordering::AcqRel);
3676 });
3677 self.submissions.push_back((number, index));
3678 if matches!(self.exposure_mode, Exposure::Auto { .. }) {
3679 self.exposure.submitted();
3680 }
3681 if let Some((buffer, x, y)) = pick_readback {
3682 let (sender, receiver) = std::sync::mpsc::channel();
3683 buffer
3684 .slice(..)
3685 .map_async(wgpu::MapMode::Read, move |result| {
3686 let _ = sender.send(result);
3687 });
3688 self.pick_readback = Some(PickReadback {
3689 buffer,
3690 receiver,
3691 x,
3692 y,
3693 });
3694 }
3695 if let Some(slot) = slot {
3696 self.profiler.submitted(slot);
3697 }
3698 let done = self.frames_done.load(Ordering::Acquire);
3699 while self
3700 .submissions
3701 .front()
3702 .is_some_and(|(frame, _)| *frame < done)
3703 {
3704 self.submissions.pop_front();
3705 }
3706 Ok(number)
3707 }
3708}
3709
3710impl Renderer {
3711 fn touch(&self) {
3712 self.revision.fetch_add(1, Ordering::Relaxed);
3713 }
3714
3715 pub fn set_frames_on_demand(&mut self, limits: Option<Limits>) -> Result<(), String> {
3716 let Some(limits) = limits else {
3717 self.demand = None;
3718 return Ok(());
3719 };
3720 if self.display.is_none() {
3721 return Err("frames on demand need an sRGB output format".into());
3722 }
3723 if self.size() != self.capacity() {
3724 return Err("frames on demand render at the full size; set the viewport to the renderer's capacity first".into());
3725 }
3726 match &mut self.demand {
3727 Some(cache) => cache.limits = limits,
3728 None => {
3729 let (width, height) = self.size();
3730 self.demand = Some(demand::Cache::new(
3731 &self.frame.gpu.device,
3732 [width, height],
3733 limits,
3734 ));
3735 }
3736 }
3737 Ok(())
3738 }
3739
3740 pub fn frames_on_demand(&self) -> Option<Limits> {
3741 self.demand.as_ref().map(|cache| cache.limits)
3742 }
3743
3744 pub fn demand_stats(&self) -> Option<DemandStats> {
3745 self.demand.as_ref().map(demand::Cache::stats)
3746 }
3747
3748 pub fn frame_needed(&self, next: &Next<'_>) -> Need {
3749 let Some(cache) = &self.demand else {
3750 return Need::Full;
3751 };
3752 demand::decide(&cache.book, &self.summarize(next), &cache.limits)
3753 }
3754
3755 pub fn submit_needed(
3756 &mut self,
3757 next: &Next<'_>,
3758 need: &Need,
3759 output: &wgpu::TextureView,
3760 ) -> Result<Option<Submitted>, String> {
3761 let Some(frame) = self.encode_needed(next, need, output)? else {
3762 return Ok(None);
3763 };
3764 Ok(Some(self.submitted_frame(frame)))
3765 }
3766
3767 pub fn render_needed(
3768 &mut self,
3769 next: &Next<'_>,
3770 need: &Need,
3771 output: &wgpu::TextureView,
3772 ) -> Result<Option<Vec<PassTiming>>, String> {
3773 let Some(frame) = self.encode_needed(next, need, output)? else {
3774 return Ok(None);
3775 };
3776 self.wait_for(frame).map(Some)
3777 }
3778
3779 pub fn present_last(&mut self, output: &wgpu::TextureView) -> Result<Submitted, String> {
3780 let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
3781 cache.cached.as_ref().ok_or("no frame has been drawn")?;
3782 let owned_device = self.frame.gpu.device.clone();
3783 let device = &owned_device;
3784 let queue = self.frame.gpu.queue.clone();
3785 self.begin_frame(FrameKind::Idle);
3786 self.pass_log.clear();
3787 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3788 label: Some("live present"),
3789 });
3790 self.encode_present(device, &mut encoder, output)?;
3791 let frame = self.finish_encoder(device, &queue, encoder, false)?;
3792 if let Some(cache) = &mut self.demand {
3793 cache.submitted();
3794 }
3795 Ok(self.submitted_frame(frame))
3796 }
3797
3798 fn encode_present(
3799 &mut self,
3800 device: &wgpu::Device,
3801 encoder: &mut wgpu::CommandEncoder,
3802 output: &wgpu::TextureView,
3803 ) -> Result<(), String> {
3804 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3805 let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
3806 let base = match cache.last {
3807 demand::Last::Warp(uniform) => {
3808 let depth = self.depths[cached.depth_index].1.clone();
3809 let colour = cache.colour.view.clone();
3810 cache.encode_warp(
3811 device,
3812 encoder,
3813 &uniform,
3814 &colour,
3815 &depth,
3816 (output, self.output_format),
3817 false,
3818 &mut self.profiler,
3819 );
3820 self.pass_log.push("reproject");
3821 return Ok(());
3822 }
3823 demand::Last::Colour => cache.colour.view.clone(),
3824 demand::Last::Scratch => cache.scratch.view.clone(),
3825 };
3826 if cache.overlaid {
3827 cache.encode_composite(
3828 device,
3829 encoder,
3830 &base,
3831 (output, self.output_format),
3832 &mut self.profiler,
3833 );
3834 self.pass_log.push("composite");
3835 } else {
3836 self.encode_blit(device, encoder, &base, output);
3837 }
3838 Ok(())
3839 }
3840
3841 fn refresh_overlay(
3842 &mut self,
3843 device: &wgpu::Device,
3844 queue: &wgpu::Queue,
3845 encoder: &mut wgpu::CommandEncoder,
3846 next: &Next<'_>,
3847 print: u64,
3848 ) -> Result<(), String> {
3849 let overlays = !next.text.overlay.is_empty()
3850 || !next.text.overlay_quads.is_empty()
3851 || next.hud.is_some();
3852 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3853 cache.overlaid = overlays;
3854 if !overlays {
3855 cache.overlay_print = None;
3856 return Ok(());
3857 }
3858 if cache.overlay_print == Some(print) {
3859 return Ok(());
3860 }
3861 cache.encode_clear(encoder);
3862 let target = cache.overlay.view.clone();
3863 cache.overlay_print = None;
3864 self.encode_overlay(device, queue, encoder, next, &target)?;
3865 if let Some(cache) = &mut self.demand {
3866 cache.overlay_print = Some(print);
3867 }
3868 Ok(())
3869 }
3870
3871 pub fn wake(&mut self, groups: u32) -> Result<Submitted, String> {
3872 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3873 let owned_device = self.frame.gpu.device.clone();
3874 let device = &owned_device;
3875 let queue = self.frame.gpu.queue.clone();
3876 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3877 label: Some("live wake"),
3878 });
3879 cache.encode_wake(device, &mut encoder, groups, &mut self.profiler);
3880 self.stats.discard();
3881 self.resolution_frame = false;
3882 self.pass_log.clear();
3883 self.pass_log.push("wake");
3884 let frame = self.finish_encoder(device, &queue, encoder, false)?;
3885 Ok(self.submitted_frame(frame))
3886 }
3887
3888 pub fn submit_bridge(
3889 &mut self,
3890 next: &Next<'_>,
3891 output: &wgpu::TextureView,
3892 ) -> Result<Option<Submitted>, String> {
3893 let Some(frame) = self.encode_bridge(next, output)? else {
3894 return Ok(None);
3895 };
3896 Ok(Some(self.submitted_frame(frame)))
3897 }
3898
3899 pub fn render_bridge(
3900 &mut self,
3901 next: &Next<'_>,
3902 output: &wgpu::TextureView,
3903 ) -> Result<Option<Vec<PassTiming>>, String> {
3904 let Some(frame) = self.encode_bridge(next, output)? else {
3905 return Ok(None);
3906 };
3907 self.wait_for(frame).map(Some)
3908 }
3909
3910 fn encode_bridge(
3911 &mut self,
3912 next: &Next<'_>,
3913 output: &wgpu::TextureView,
3914 ) -> Result<Option<u64>, String> {
3915 let Some(cache) = &self.demand else {
3916 return Ok(None);
3917 };
3918 let Some(cached) = &cache.cached else {
3919 return Ok(None);
3920 };
3921 let camera = next.scene.camera;
3922 if demand::motion(
3923 (&cached.view, &cached.projection, [0.0; 3]),
3924 (&camera.view, &camera.projection, [0.0; 3]),
3925 None,
3926 )
3927 .is_none()
3928 {
3929 return Ok(None);
3930 }
3931 let summary = self.summarize(next);
3932 self.begin_frame(FrameKind::Reproject);
3933 let frame = self.encode_reproject(next, &summary, output)?;
3934 if let Some(cache) = &mut self.demand {
3935 cache.submitted();
3936 }
3937 Ok(Some(frame))
3938 }
3939
3940 fn layer_reach(&self, style: Finish) -> Option<u32> {
3941 if self.lens.is_some() {
3942 return None;
3943 }
3944 if (self.custom_finish || style == self.style)
3945 && let Some(reach) = self.post.region_reach()
3946 {
3947 return Some(reach);
3948 }
3949 self.custom_local
3950 .unwrap_or_else(|| demand::local_post(&chain(style, false)))
3951 .then_some(0)
3952 }
3953
3954 fn summarize(&self, next: &Next<'_>) -> demand::Summary {
3955 let (width, height) = self.size();
3956 let camera = next.scene.camera;
3957 let view_projection = frame::multiply(camera.projection, camera.view);
3958 let mut world = Print::new();
3959 world.wide(self.revision.load(Ordering::Relaxed));
3960 world.wide(next.content);
3961 world.debug(&next.style);
3962 demand::print_scene(&mut world, next.scene);
3963 demand::print_effects(&mut world, next.effects);
3964 let mut overlay = Print::new();
3965 overlay.wide(next.overlay);
3966 overlay.word(u32::from(next.hud.is_some()));
3967 let mut anchors = Vec::new();
3968 demand::print_text(
3969 &mut world,
3970 &mut overlay,
3971 &mut anchors,
3972 next.text,
3973 &view_projection,
3974 );
3975 let auto = matches!(self.exposure_mode, Exposure::Auto { .. });
3976 let ticking = (next.effects.creature_mesh.is_some() && !next.effects.creatures.is_empty())
3977 || next.effects.liquid.is_some()
3978 || self.frame.has_tree()
3979 || next.scene.wind.current.strength > 0.0
3980 || self.tape.is_some_and(|tape| !tape.is_off())
3981 || self.sky_settling()
3982 || (auto
3983 && self
3984 .demand
3985 .as_ref()
3986 .is_some_and(|cache| cache.exposure_moving));
3987 let reach = self.layer_reach(next.style);
3988 let whole = Region::whole(width, height);
3989 let mut layered = Vec::new();
3990 if let Some(volume) = &next.effects.volume
3991 && let Some(region) = demand::box_region(
3992 &view_projection,
3993 volume.lo,
3994 volume.hi,
3995 8 + demand::MARGIN + reach.unwrap_or(0),
3996 width,
3997 height,
3998 )
3999 {
4000 layered.push(region);
4001 }
4002 if let Some(region) = demand::particle_region(
4003 &view_projection,
4004 next.effects.particles,
4005 demand::MARGIN + reach.unwrap_or(0),
4006 width,
4007 height,
4008 ) {
4009 layered.push(region);
4010 }
4011 if reach.is_none() && !layered.is_empty() {
4012 layered = vec![whole];
4013 }
4014 demand::Summary {
4015 view: camera.view,
4016 projection: camera.projection,
4017 position: camera.position,
4018 size: [width, height],
4019 world: world.finish(),
4020 anchors,
4021 overlay: overlay.finish(),
4022 ticking,
4023 layered: demand::merge(&layered, width, height),
4024 moving: next
4025 .moving
4026 .iter()
4027 .map(|region| region.clip(width, height))
4028 .filter(|region| !region.is_empty())
4029 .collect(),
4030 }
4031 }
4032
4033 fn encode_needed(
4034 &mut self,
4035 next: &Next<'_>,
4036 need: &Need,
4037 output: &wgpu::TextureView,
4038 ) -> Result<Option<u64>, String> {
4039 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4040 let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
4041 cache.gather();
4042 let need = if cache.cached.is_none() && need.draws() {
4043 &Need::Full
4044 } else {
4045 need
4046 };
4047 let summary = self.summarize(next);
4048 let (frame, kind) = match need {
4049 Need::None => return Ok(None),
4050 Need::Full => {
4051 self.begin_frame(FrameKind::Full);
4052 (
4053 self.encode_frame(
4054 next.scene,
4055 next.text,
4056 next.effects,
4057 next.style,
4058 next.hud,
4059 output,
4060 true,
4061 )?,
4062 Shown::Full,
4063 )
4064 }
4065 Need::Reproject => {
4066 self.begin_frame(FrameKind::Reproject);
4067 (
4068 self.encode_reproject(next, &summary, output)?,
4069 Shown::Reproject,
4070 )
4071 }
4072 Need::Partial(regions) => {
4073 self.begin_frame(FrameKind::Partial);
4074 (
4075 self.encode_partial(next, &summary, regions, output)?,
4076 Shown::Partial,
4077 )
4078 }
4079 };
4080 let summary = if kind == Shown::Full {
4081 self.summarize(next)
4082 } else {
4083 summary
4084 };
4085 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4086 cache.submitted();
4087 if kind == Shown::Full && cache.overlaid {
4088 cache.overlay_print = Some(summary.overlay);
4089 }
4090 let limits = cache.limits;
4091 demand::record(&mut cache.book, summary, kind, &limits);
4092 Ok(Some(frame))
4093 }
4094
4095 fn layer_frame(&self, scene: &Scene<'_>, view_projection: Matrix) -> EffectFrame {
4096 let (width, height) = self.size();
4097 let view = camera_view(scene, width, height);
4098 let camera = scene.camera;
4099 let right = [camera.view[0][0], camera.view[1][0], camera.view[2][0]];
4100 let mut frame = EffectFrame::new(width, height, scene.time, scene.seed);
4101 frame.view_proj = view_projection;
4102 frame.eye = [
4103 camera.position[0],
4104 camera.position[1],
4105 camera.position[2],
4106 0.0,
4107 ];
4108 frame.right = [right[0], right[1], right[2], 0.0];
4109 frame.up = [view.up[0], view.up[1], view.up[2], 0.0];
4110 frame.forward = [view.forward[0], view.forward[1], view.forward[2], 0.0];
4111 frame.lens = [
4112 1.0 / camera.projection[0][0],
4113 1.0 / camera.projection[1][1],
4114 0.0,
4115 0.0,
4116 ];
4117 frame.sun = [
4118 scene.sun.direction[0],
4119 scene.sun.direction[1],
4120 scene.sun.direction[2],
4121 0.0,
4122 ];
4123 frame.sun_color = [
4124 scene.sun.colour[0],
4125 scene.sun.colour[1],
4126 scene.sun.colour[2],
4127 scene.sun.intensity,
4128 ];
4129 frame
4130 }
4131
4132 #[allow(clippy::too_many_arguments)]
4133 fn encode_layer_post(
4134 &mut self,
4135 device: &wgpu::Device,
4136 encoder: &mut wgpu::CommandEncoder,
4137 next: &Next<'_>,
4138 depth: &wgpu::TextureView,
4139 target: &wgpu::TextureView,
4140 rects: Option<&[Region]>,
4141 ) -> Result<(), String> {
4142 let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
4143 let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4144 let camera = next.scene.camera;
4145 let view_projection = frame::multiply(camera.projection, camera.view);
4146 let layer_frame = self.layer_frame(next.scene, view_projection);
4147 let layer = cache.layer.view.clone();
4148 let shadow_data = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
4149 label: Some("demand sun cascades"),
4150 contents: &cached.shadow,
4151 usage: wgpu::BufferUsages::UNIFORM,
4152 });
4153 let (frame_number, exposure) = (cached.frame_number, cached.exposure);
4154 draw_layers(
4155 device,
4156 encoder,
4157 &mut self.effects,
4158 &mut self.profiler,
4159 next.effects,
4160 &layer_frame,
4161 EffectTargets {
4162 hdr: &layer,
4163 depth,
4164 shadow: Some((self.shadows.atlas_view(), &shadow_data)),
4165 },
4166 &mut self.pass_log,
4167 );
4168 let source = if let Some(lens) = self.lens {
4169 let timing = self.profiler.pass("depth of field");
4170 self.pass_log.push("depth of field");
4171 self.dof.render(
4172 encoder,
4173 &layer,
4174 depth,
4175 lens,
4176 dof::height_plane(camera.view, camera.projection),
4177 self.profiler.compute_writes(timing),
4178 )
4179 } else {
4180 &layer
4181 };
4182 match rects {
4183 Some(rects) => {
4184 let rects: Vec<[u32; 4]> = rects
4185 .iter()
4186 .map(|r| [r.x, r.y, r.x + r.width, r.y + r.height])
4187 .collect();
4188 self.post.run_region(
4189 encoder,
4190 source,
4191 Some(&self.frame.targets.depth_view),
4192 Some(&self.frame.targets.normal_roughness_view),
4193 target,
4194 frame_number,
4195 next.scene.seed,
4196 exposure,
4197 &rects,
4198 Some(&mut self.profiler),
4199 );
4200 }
4201 None => self.post.run_timed_exposed(
4202 encoder,
4203 source,
4204 Some(&self.frame.targets.depth_view),
4205 Some(&self.frame.targets.normal_roughness_view),
4206 target,
4207 frame_number,
4208 next.scene.seed,
4209 exposure,
4210 Some(&mut self.profiler),
4211 ),
4212 }
4213 self.pass_log.push("post");
4214 Ok(())
4215 }
4216
4217 fn encode_overlay(
4218 &mut self,
4219 device: &wgpu::Device,
4220 queue: &wgpu::Queue,
4221 encoder: &mut wgpu::CommandEncoder,
4222 next: &Next<'_>,
4223 target: &wgpu::TextureView,
4224 ) -> Result<(), String> {
4225 let (width, height) = self.size();
4226 let text = next.text;
4227 for item in &text.overlay {
4228 self.text
4229 .encode_timed(
4230 device,
4231 encoder,
4232 TextDraw {
4233 target,
4234 depth: None,
4235 atlas: item.atlas,
4236 paragraph: item.paragraph,
4237 space: item.space,
4238 color: item.color,
4239 deformers: None,
4240 },
4241 Some(&mut self.profiler),
4242 )
4243 .map_err(str::to_owned)?;
4244 }
4245 for item in &text.overlay_quads {
4246 self.text
4247 .encode_quads_timed(
4248 device,
4249 encoder,
4250 QuadDraw {
4251 target,
4252 depth: None,
4253 atlas: item.atlas,
4254 quads: item.quads,
4255 space: item.space,
4256 deformers: None,
4257 icons: item.icons,
4258 },
4259 Some(&mut self.profiler),
4260 )
4261 .map_err(str::to_owned)?;
4262 }
4263 if !text.overlay.is_empty() || !text.overlay_quads.is_empty() {
4264 self.pass_log.push("overlay text");
4265 }
4266 if let Some(hud) = next.hud {
4267 let mut flat = self
4268 .flat
4269 .take()
4270 .unwrap_or_else(|| FlatPass::new(device, queue));
4271 let prepared = flat.prepare(device, queue, hud, [width, height]);
4272 if let Err(error) = prepared {
4273 self.flat = Some(flat);
4274 return Err(error);
4275 }
4276 let colour = flat.colour_target(device, [width, height]);
4277 flat.encode_colour(encoder, &colour, Some([0.0; 4]), Some(&mut self.profiler));
4278 flat.encode_finish(
4279 device,
4280 encoder,
4281 &colour,
4282 target,
4283 wgpu::TextureFormat::Rgba16Float,
4284 self.display.is_some(),
4285 true,
4286 Some(&mut self.profiler),
4287 );
4288 self.flat = Some(flat);
4289 self.pass_log.extend(["flat", "flat finish"]);
4290 }
4291 Ok(())
4292 }
4293
4294 fn encode_reproject(
4295 &mut self,
4296 next: &Next<'_>,
4297 summary: &demand::Summary,
4298 output: &wgpu::TextureView,
4299 ) -> Result<u64, String> {
4300 self.pass_log.clear();
4301 let (width, height) = self.size();
4302 self.text_bounds = text_bounds(next.text, [width, height]);
4303 let owned_device = self.frame.gpu.device.clone();
4304 let device = &owned_device;
4305 let queue = self.frame.gpu.queue.clone();
4306 let camera = next.scene.camera;
4307 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4308 label: Some("live reproject"),
4309 });
4310 self.refresh_overlay(device, &queue, &mut encoder, next, summary.overlay)?;
4311 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4312 let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4313 let mut uniform = demand::WarpUniform::new(
4314 (&cached.view, &cached.projection),
4315 (&camera.view, &camera.projection),
4316 [width, height],
4317 cached.far,
4318 )
4319 .ok_or("camera projection is singular")?;
4320 let depth = self.depths[cached.depth_index].1.clone();
4321 if !summary.layered.is_empty() || cache.colour_layers {
4322 let still = cache.still.view.clone();
4323 let layer = cache.layer.view.clone();
4324 let scratch = cache.scratch.view.clone();
4325 cache.encode_warp(
4326 device,
4327 &mut encoder,
4328 &uniform,
4329 &still,
4330 &depth,
4331 (&layer, wgpu::TextureFormat::Rgba16Float),
4332 true,
4333 &mut self.profiler,
4334 );
4335 cache.last = demand::Last::Scratch;
4336 let warped = cache.depth.view.clone();
4337 self.pass_log.push("reproject");
4338 self.encode_layer_post(device, &mut encoder, next, &warped, &scratch, None)?;
4339 self.encode_present(device, &mut encoder, output)?;
4340 } else {
4341 uniform.limits[3] = if cache.overlaid { 1.0 } else { 0.0 };
4342 cache.last = demand::Last::Warp(uniform);
4343 self.encode_present(device, &mut encoder, output)?;
4344 }
4345 self.finish_encoder(device, &queue, encoder, false)
4346 }
4347
4348 fn encode_partial(
4349 &mut self,
4350 next: &Next<'_>,
4351 summary: &demand::Summary,
4352 regions: &[Region],
4353 output: &wgpu::TextureView,
4354 ) -> Result<u64, String> {
4355 self.pass_log.clear();
4356 let (width, height) = self.size();
4357 self.text_bounds = text_bounds(next.text, [width, height]);
4358 let owned_device = self.frame.gpu.device.clone();
4359 let device = &owned_device;
4360 let queue = self.frame.gpu.queue.clone();
4361 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4362 label: Some("live partial"),
4363 });
4364 let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4365 let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4366 let depth = self.depths[cached.depth_index].1.clone();
4367 let mut layered = summary.layered.clone();
4368 if let Some(shown) = &cache.book.shown {
4369 layered.extend(shown.layered.iter().copied());
4370 }
4371 let layered: Vec<Region> = demand::merge(&layered, width, height)
4372 .into_iter()
4373 .filter(|region| regions.iter().any(|outer| outer.touches(region, 0)))
4374 .collect();
4375 let colour = cache.colour.view.clone();
4376 cache.last = demand::Last::Colour;
4377 if !layered.is_empty() {
4378 let still = cache.still.view.clone();
4379 let layer = cache.layer.view.clone();
4380 let scratch = cache.scratch.view.clone();
4381 cache.encode_copy(
4382 device,
4383 &mut encoder,
4384 &still,
4385 &layer,
4386 &layered,
4387 "partial still",
4388 &mut self.profiler,
4389 );
4390 cache.colour_layers = !summary.layered.is_empty() || cache.colour_layers;
4391 self.pass_log.push("partial still");
4392 self.encode_layer_post(device, &mut encoder, next, &depth, &scratch, Some(&layered))?;
4393 let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
4394 cache.encode_copy(
4395 device,
4396 &mut encoder,
4397 &scratch,
4398 &colour,
4399 &layered,
4400 "partial colour",
4401 &mut self.profiler,
4402 );
4403 self.pass_log.push("partial colour");
4404 }
4405 self.refresh_overlay(device, &queue, &mut encoder, next, summary.overlay)?;
4406 self.encode_present(device, &mut encoder, output)?;
4407 self.finish_encoder(device, &queue, encoder, false)
4408 }
4409}
4410
4411#[allow(clippy::too_many_arguments)]
4412fn draw_layers(
4413 device: &wgpu::Device,
4414 encoder: &mut wgpu::CommandEncoder,
4415 pass: &mut effects::Effects,
4416 profiler: &mut GpuProfiler,
4417 effects: &Effects<'_>,
4418 frame: &EffectFrame,
4419 targets: EffectTargets<'_>,
4420 log: &mut Vec<&'static str>,
4421) {
4422 if let Some(volume) = &effects.volume {
4423 let mut volume_frame = *frame;
4424 volume_frame.set_volume(
4425 volume.kind,
4426 volume.lo,
4427 volume.hi,
4428 volume.color,
4429 volume.density,
4430 volume.anisotropy,
4431 );
4432 volume_frame.scale_time(volume.time_scale);
4433 let march = profiler.pass("volume march");
4434 let upsample = profiler.pass("volume upsample");
4435 pass.render_volume(
4436 &mut EffectCommands { device, encoder },
4437 &volume_frame,
4438 targets,
4439 VolumeTimestamps {
4440 march: profiler.render_writes(march),
4441 upsample: profiler.render_writes(upsample),
4442 },
4443 );
4444 log.extend(["volume march", "volume upsample"]);
4445 }
4446 if !effects.particles.is_empty() {
4447 let timing = profiler.pass("particles");
4448 pass.render_particles(
4449 &mut EffectCommands { device, encoder },
4450 frame,
4451 targets,
4452 effects.particles,
4453 profiler.render_writes(timing),
4454 );
4455 log.push("particles");
4456 }
4457}
4458
4459#[allow(clippy::too_many_arguments)]
4460fn draw_surface_text(
4461 pass: &mut TextPass,
4462 frame: &Frame,
4463 profiler: &mut GpuProfiler,
4464 device: &wgpu::Device,
4465 encoder: &mut wgpu::CommandEncoder,
4466 text: &Text<'_>,
4467 deformers: &wgpu::BindGroup,
4468 target: &wgpu::TextureView,
4469) -> Result<(), String> {
4470 for item in &text.surface {
4471 let draw = TextDraw {
4472 target,
4473 depth: Some(&frame.targets.depth_view),
4474 atlas: item.atlas,
4475 paragraph: item.paragraph,
4476 space: item.space,
4477 color: item.color,
4478 deformers: Some(deformers),
4479 };
4480 if item.space.is_lit() {
4481 let lit = frame
4482 .lit_bindings()
4483 .ok_or("frame lighting is not prepared")?;
4484 pass.encode_lit_timed(device, encoder, draw, &lit, Some(&mut *profiler))
4485 } else {
4486 pass.encode_timed(device, encoder, draw, Some(&mut *profiler))
4487 }
4488 .map_err(str::to_owned)?;
4489 }
4490 for item in &text.surface_quads {
4491 let draw = QuadDraw {
4492 target,
4493 depth: Some(&frame.targets.depth_view),
4494 atlas: item.atlas,
4495 quads: item.quads,
4496 space: item.space,
4497 deformers: Some(deformers),
4498 icons: item.icons,
4499 };
4500 if item.space.is_lit() {
4501 let lit = frame
4502 .lit_bindings()
4503 .ok_or("frame lighting is not prepared")?;
4504 pass.encode_quads_lit_timed(device, encoder, draw, &lit, Some(&mut *profiler))
4505 } else {
4506 pass.encode_quads_timed(device, encoder, draw, Some(&mut *profiler))
4507 }
4508 .map_err(str::to_owned)?;
4509 }
4510 Ok(())
4511}
4512
4513fn text_bounds(text: &Text<'_>, size: [u32; 2]) -> Vec<TextBounds> {
4514 let paragraphs = |list: TextList, items: &[TextItem<'_>]| {
4515 items
4516 .iter()
4517 .enumerate()
4518 .filter_map(|(index, item)| {
4519 crate::text::screen_bounds(
4520 &item.space,
4521 item.paragraph.glyphs.iter().map(PackedGlyph::from),
4522 size,
4523 )
4524 .map(|rect| TextBounds {
4525 list,
4526 index,
4527 id: item.id,
4528 rect,
4529 })
4530 })
4531 .collect::<Vec<_>>()
4532 };
4533 let quads = |list: TextList, items: &[TextQuadItem<'_>]| {
4534 items
4535 .iter()
4536 .enumerate()
4537 .filter_map(|(index, item)| {
4538 crate::text::screen_bounds(
4539 &item.space,
4540 item.quads.iter().map(PackedGlyph::from),
4541 size,
4542 )
4543 .map(|rect| TextBounds {
4544 list,
4545 index,
4546 id: item.id,
4547 rect,
4548 })
4549 })
4550 .collect::<Vec<_>>()
4551 };
4552 let mut bounds = paragraphs(TextList::Surface, &text.surface);
4553 bounds.extend(quads(TextList::SurfaceQuads, &text.surface_quads));
4554 bounds.extend(paragraphs(TextList::Overlay, &text.overlay));
4555 bounds.extend(quads(TextList::OverlayQuads, &text.overlay_quads));
4556 bounds
4557}
4558
4559fn readback_texture(
4560 gpu: &Gpu,
4561 texture: &wgpu::Texture,
4562 width: u32,
4563 height: u32,
4564 bytes_per_pixel: u32,
4565) -> Result<Vec<u8>, String> {
4566 let raw_row = width * bytes_per_pixel;
4567 let row =
4568 raw_row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
4569 let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
4570 label: Some("live geometry readback"),
4571 size: u64::from(row) * u64::from(height),
4572 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4573 mapped_at_creation: false,
4574 });
4575 let mut encoder = gpu
4576 .device
4577 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4578 label: Some("live geometry readback"),
4579 });
4580 encoder.copy_texture_to_buffer(
4581 texture.as_image_copy(),
4582 wgpu::TexelCopyBufferInfo {
4583 buffer: &buffer,
4584 layout: wgpu::TexelCopyBufferLayout {
4585 offset: 0,
4586 bytes_per_row: Some(row),
4587 rows_per_image: Some(height),
4588 },
4589 },
4590 wgpu::Extent3d {
4591 width,
4592 height,
4593 depth_or_array_layers: 1,
4594 },
4595 );
4596 gpu.queue.submit(Some(encoder.finish()));
4597 let (sender, receiver) = std::sync::mpsc::channel();
4598 let slice = buffer.slice(..);
4599 slice.map_async(wgpu::MapMode::Read, move |result| {
4600 let _ = sender.send(result);
4601 });
4602 gpu.device
4603 .poll(wgpu::PollType::wait_indefinitely())
4604 .map_err(|error| format!("{error:?}"))?;
4605 receiver
4606 .recv()
4607 .map_err(|error| error.to_string())?
4608 .map_err(|error| error.to_string())?;
4609 let mapped = slice.get_mapped_range().expect("a mapped range");
4610 let mut bytes = Vec::with_capacity((raw_row * height) as usize);
4611 for padded in mapped.chunks_exact(row as usize) {
4612 bytes.extend_from_slice(&padded[..raw_row as usize]);
4613 }
4614 drop(mapped);
4615 buffer.unmap();
4616 Ok(bytes)
4617}
4618
4619#[cfg(test)]
4620mod tests {
4621 use super::*;
4622 use crate::frame::{Camera, SceneWind, Sun};
4623 use crate::sky::AnalyticSky;
4624 use pfx_geom::shapes::Shape;
4625 use pfx_gpu::OffscreenTarget;
4626 use pfx_materials::Material;
4627
4628 fn identity() -> Matrix {
4629 [
4630 [1.0, 0.0, 0.0, 0.0],
4631 [0.0, 1.0, 0.0, 0.0],
4632 [0.0, 0.0, 1.0, 0.0],
4633 [0.0, 0.0, 0.0, 1.0],
4634 ]
4635 }
4636
4637 fn perspective(aspect: f32) -> Matrix {
4638 let near = 0.1;
4639 let far = 100.0;
4640 let f = 1.0 / (40.0_f32.to_radians() * 0.5).tan();
4641 [
4642 [f / aspect, 0.0, 0.0, 0.0],
4643 [0.0, f, 0.0, 0.0],
4644 [0.0, 0.0, far / (near - far), -1.0],
4645 [0.0, 0.0, far * near / (near - far), 0.0],
4646 ]
4647 }
4648
4649 #[test]
4650 fn fixed_and_biased_exposure() {
4651 assert_eq!(Exposure::Fixed(0.93).value(4.0), 0.93);
4652 assert_eq!(Exposure::Auto { bias: 1.0 }.value(0.75), 1.5);
4653 assert_eq!(Exposure::Auto { bias: -1.0 }.value(0.75), 0.375);
4654 }
4655
4656 #[test]
4657 fn plain_passes_merge_post_and_keep_sixteen() {
4658 let mut passes = (0..16)
4659 .map(|index| PassTiming {
4660 label: format!("pass {index}"),
4661 milliseconds: index as f64,
4662 })
4663 .collect::<Vec<_>>();
4664 passes.extend(
4665 ["post 0:0", "post 1:0", "post 2:0", "post 3:0"].map(|label| PassTiming {
4666 label: label.to_owned(),
4667 milliseconds: 5.0,
4668 }),
4669 );
4670 passes.push(PassTiming {
4671 label: "volume march".into(),
4672 milliseconds: 0.2,
4673 });
4674 passes.push(PassTiming {
4675 label: "volume upsample".into(),
4676 milliseconds: 0.3,
4677 });
4678 let merged = plain_passes(passes);
4679 assert_eq!(merged.len(), 16);
4680 assert_eq!(merged[0].label, "post");
4681 assert_eq!(merged[0].milliseconds, 20.0);
4682 assert!(merged.iter().all(|pass| !pass.label.starts_with("post ")));
4683 assert!(!merged.iter().any(|pass| pass.label == "pass 0"));
4684 }
4685
4686 #[test]
4687 #[ignore = "needs a GPU; run under pgpu"]
4688 fn caller_finish_matches_gpu_chain() {
4689 let gpu = pollster::block_on(Gpu::headless()).unwrap();
4690 let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
4691 let mut finish = Chain::new();
4692 finish.passes = vec![
4693 PostPass::Exposure(1.25),
4694 PostPass::Bloom(Bloom::neutral(0.2)),
4695 PostPass::Tone(Tone::aces()),
4696 ];
4697 renderer.set_finish(finish.clone());
4698 let mut expected = finish;
4699 expected.passes.insert(0, PostPass::Exposure(1.0));
4700 expected.passes.push(PostPass::Encode);
4701 let direct = GpuChain::new(expected, &renderer.gpu().device, 32, 32);
4702 let input = output(renderer.gpu(), 32, 32);
4703 let actual = output(renderer.gpu(), 32, 32);
4704 let reference = output(renderer.gpu(), 32, 32);
4705 let mut encoder = renderer
4706 .gpu()
4707 .device
4708 .create_command_encoder(&Default::default());
4709 {
4710 let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4711 multiview_mask: None,
4712 label: Some("finish input"),
4713 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4714 view: &input.view,
4715 resolve_target: None,
4716 depth_slice: None,
4717 ops: wgpu::Operations {
4718 load: wgpu::LoadOp::Clear(wgpu::Color {
4719 r: 1.5,
4720 g: 0.7,
4721 b: 0.2,
4722 a: 1.0,
4723 }),
4724 store: wgpu::StoreOp::Store,
4725 },
4726 })],
4727 depth_stencil_attachment: None,
4728 timestamp_writes: None,
4729 occlusion_query_set: None,
4730 });
4731 }
4732 renderer
4733 .post
4734 .run(&mut encoder, &input.view, None, None, &actual.view, 7, 9);
4735 direct.run(&mut encoder, &input.view, None, None, &reference.view, 7, 9);
4736 renderer.gpu().queue.submit(Some(encoder.finish()));
4737 assert_eq!(
4738 renderer.gpu().readback_rgba16(&actual).unwrap(),
4739 renderer.gpu().readback_rgba16(&reference).unwrap()
4740 );
4741 }
4742
4743 #[test]
4744 #[ignore = "needs a GPU; run under pgpu"]
4745 fn set_tape_overrides_the_finish_until_cleared() {
4746 let gpu = pollster::block_on(Gpu::headless()).unwrap();
4747 let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
4748 assert!(renderer.post.tape().is_off());
4749 let mut finish = Chain::new();
4750 finish.passes.push(PostPass::Tone(Tone::aces()));
4751 finish.set_tape(Tape::forward(0.5));
4752 renderer.set_finish(finish);
4753 assert_eq!(renderer.post.tape(), Tape::forward(0.5));
4754 renderer.set_tape(Some(Tape::rewind(1.0)));
4755 assert_eq!(renderer.post.tape(), Tape::rewind(1.0));
4756 renderer.set_tape(None);
4757 assert_eq!(renderer.post.tape(), Tape::forward(0.5));
4758 renderer.clear_finish();
4759 renderer.set_tape(Some(Tape::rewind(0.3)));
4760 assert_eq!(renderer.post.tape(), Tape::rewind(0.3));
4761 renderer.set_tape(None);
4762 assert!(renderer.post.tape().is_off());
4763 }
4764
4765 fn sphere_still(size: u32) -> (Renderer, OffscreenTarget, Vec<Instance>, Vec<Material>) {
4766 let gpu = pollster::block_on(Gpu::headless()).unwrap();
4767 let mut renderer = Renderer::new(gpu, size, size).unwrap();
4768 renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
4769 let shape = Shape::Ellipsoid {
4770 radius: 0.7,
4771 squash: 1.0,
4772 }
4773 .mesh(0.05);
4774 let mesh = renderer
4775 .upload_mesh(MeshData {
4776 positions: &shape.positions,
4777 normals: &shape.normals,
4778 tangents: &shape.tangents,
4779 uvs: &shape.uvs,
4780 uvs1: None,
4781 alpha: None,
4782 indices: &shape.indices,
4783 })
4784 .unwrap();
4785 let target = output(renderer.gpu(), size, size);
4786 let materials = vec![Material {
4787 base: [0.9; 3],
4788 metalness: 1.0,
4789 roughness: 0.3,
4790 ..Default::default()
4791 }];
4792 (
4793 renderer,
4794 target,
4795 vec![Instance::new(mesh, identity(), 0, 5)],
4796 materials,
4797 )
4798 }
4799
4800 fn still_scene<'a>(
4801 instances: &'a [Instance],
4802 materials: &'a [Material],
4803 number: u32,
4804 ) -> Scene<'a> {
4805 let shift = |n: u32| (n as f32 * 0.05).sin() * 0.3;
4806 let projection = perspective(1.0);
4807 let mut view = identity();
4808 view[3][0] = shift(number);
4809 view[3][2] = -3.0;
4810 let mut before = view;
4811 before[3][0] = shift(number.saturating_sub(1));
4812 Scene {
4813 camera: Camera {
4814 view,
4815 projection,
4816 previous_view_projection: frame::multiply(projection, before),
4817 position: [-view[3][0], 0.0, 3.0],
4818 },
4819 time: number as f32 / 60.0,
4820 seed: 11,
4821 sun: Sun {
4822 direction: [0.3, 0.8, 0.5],
4823 colour: [1.0; 3],
4824 intensity: 2.0,
4825 },
4826 instances,
4827 materials,
4828 deformers: &[],
4829 wind: SceneWind::default(),
4830 }
4831 }
4832
4833 fn varied_still(size: u32) -> (Renderer, OffscreenTarget, Vec<Instance>, Vec<Material>) {
4834 let (renderer, target, mut instances, materials) = sphere_still(size);
4835 let sphere = instances[0];
4836 let mut two_sided = sphere;
4837 two_sided.model = translated(-1.2, 0.0, 0.0);
4838 two_sided.previous_model = two_sided.model;
4839 two_sided.two_sided = true;
4840 two_sided.id = 6;
4841 let mut aged = sphere;
4842 aged.model = translated(1.2, 0.0, 0.0);
4843 aged.previous_model = aged.model;
4844 aged.age = 0.6;
4845 aged.id = 7;
4846 instances.extend([two_sided, aged]);
4847 (renderer, target, instances, materials)
4848 }
4849
4850 fn sorted(mut keys: Vec<OpaqueKey>) -> Vec<OpaqueKey> {
4851 keys.sort_by_key(|key| (key.features.flags(), key.lit, key.two_sided));
4852 keys
4853 }
4854
4855 fn draw_frames(
4856 renderer: &mut Renderer,
4857 target: &OffscreenTarget,
4858 instances: &[Instance],
4859 materials: &[Material],
4860 frames: u32,
4861 ) -> Vec<Vec<u16>> {
4862 (0..frames)
4863 .map(|number| {
4864 renderer
4865 .render(
4866 &still_scene(instances, materials, number),
4867 &Text::default(),
4868 &Effects::default(),
4869 Finish::Standard,
4870 &target.view,
4871 )
4872 .unwrap();
4873 renderer.gpu().readback_rgba16(target).unwrap()
4874 })
4875 .collect()
4876 }
4877
4878 #[test]
4879 #[ignore = "needs a GPU; run under pgpu"]
4880 fn warm_makes_the_opaque_pipelines_the_frames_draw_and_the_same_bytes() {
4881 let (mut warmed, target, instances, materials) = varied_still(64);
4882 let warming = warmed
4883 .warm(&[still_scene(&instances, &materials, 0)])
4884 .unwrap();
4885 let progress = warming.wait();
4886 assert!(warming.done() && warming.first_ready());
4887 assert_eq!(progress.failed, 0);
4888 assert_eq!(progress.total, 3);
4889 assert_eq!(progress.first_total, 3);
4890 assert!(warmed.frame().opaque_pipelines().len() <= 3);
4891 let warm_frames = draw_frames(&mut warmed, &target, &instances, &materials, 3);
4892 assert_eq!(warmed.frame().opaque_made_on_render_thread(), 0);
4893 let (mut cold, cold_target, cold_instances, cold_materials) = varied_still(64);
4894 let cold_frames = draw_frames(&mut cold, &cold_target, &cold_instances, &cold_materials, 3);
4895 assert_eq!(cold.frame().opaque_made_on_render_thread(), 3);
4896 assert_eq!(
4897 sorted(warmed.frame().opaque_pipelines()),
4898 sorted(cold.frame().opaque_pipelines())
4899 );
4900 for (number, (warm, cold)) in warm_frames.iter().zip(&cold_frames).enumerate() {
4901 assert!(warm == cold, "frame {number} differs after the warm");
4902 }
4903 let again = warmed
4904 .warm(&[still_scene(&instances, &materials, 3)])
4905 .unwrap();
4906 assert_eq!(again.progress().total, 0);
4907 assert!(again.done());
4908 }
4909
4910 #[test]
4911 #[ignore = "needs a GPU; run under pgpu"]
4912 fn later_scenes_warm_after_the_first_and_a_frame_waits_for_its_own() {
4913 let (mut renderer, target, instances, materials) = varied_still(64);
4914 let plain: Vec<Instance> = instances[..1].to_vec();
4915 renderer.set_warm_workers(1);
4916 let warming = renderer
4917 .warm(&[
4918 still_scene(&plain, &materials, 0),
4919 still_scene(&instances, &materials, 0),
4920 ])
4921 .unwrap();
4922 let started = warming.progress();
4923 assert_eq!((started.first_total, started.total), (1, 3));
4924 draw_frames(&mut renderer, &target, &plain, &materials, 1);
4925 assert_eq!(renderer.frame().opaque_made_on_render_thread(), 0);
4926 assert!(warming.first_ready());
4927 warming.wait();
4928 draw_frames(&mut renderer, &target, &instances, &materials, 2);
4929 assert_eq!(renderer.frame().opaque_made_on_render_thread(), 0);
4930 assert_eq!(renderer.frame().opaque_pipelines().len(), 3);
4931 }
4932
4933 #[test]
4934 #[ignore = "needs a GPU; run under pgpu"]
4935 fn the_first_frame_draws_the_same_bytes_through_the_reflection_pipelines() {
4936 let ssr = frame::OpaqueFeatures::named("ssr").unwrap();
4937 let (mut merged, target, instances, materials) = varied_still(64);
4938 let (mut split, split_target, split_instances, split_materials) = varied_still(64);
4939 split.frame().set_opaque_off(ssr);
4940 let first = draw_frames(&mut merged, &target, &instances, &materials, 1);
4941 let before = draw_frames(
4942 &mut split,
4943 &split_target,
4944 &split_instances,
4945 &split_materials,
4946 1,
4947 );
4948 assert!(merged.frame().opaque_drawn().iter().all(|f| f.ssr));
4949 assert!(split.frame().opaque_drawn().iter().all(|f| !f.ssr));
4950 assert!(first[0] == before[0], "the first frame changed");
4951 split.frame().set_opaque_off(frame::OpaqueFeatures::NONE);
4952 let made = merged.frame().opaque_made_on_render_thread();
4953 for number in 1..3 {
4954 let scene = still_scene(&instances, &materials, number);
4955 let split_scene = still_scene(&split_instances, &split_materials, number);
4956 for (renderer, scene, target) in [
4957 (&mut merged, &scene, &target),
4958 (&mut split, &split_scene, &split_target),
4959 ] {
4960 renderer
4961 .render(
4962 scene,
4963 &Text::default(),
4964 &Effects::default(),
4965 Finish::Standard,
4966 &target.view,
4967 )
4968 .unwrap();
4969 }
4970 assert!(
4971 merged.gpu().readback_rgba16(&target).unwrap()
4972 == split.gpu().readback_rgba16(&split_target).unwrap(),
4973 "frame {number} differs"
4974 );
4975 }
4976 assert_eq!(merged.frame().opaque_made_on_render_thread(), made);
4977 }
4978
4979 fn cache_path(name: &str) -> std::path::PathBuf {
4980 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4981 .join("../../tmp/pipeline-cache-tests")
4982 .join(name);
4983 let _ = std::fs::remove_file(&path);
4984 path
4985 }
4986
4987 fn caches(gpu: &Gpu) -> bool {
4988 gpu.device
4989 .features()
4990 .contains(wgpu::Features::PIPELINE_CACHE)
4991 && crate::pipeline_cache::cache_key(&gpu.info).is_some()
4992 }
4993
4994 #[test]
4995 #[ignore = "needs a GPU; run under pgpu"]
4996 fn a_pipeline_cache_round_trips_where_the_backend_keeps_one() {
4997 let path = cache_path("round-trip.bin");
4998 let gpu = pollster::block_on(Gpu::headless()).unwrap();
4999 let supported = caches(&gpu);
5000 let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
5001 let load = renderer.use_pipeline_cache(&path);
5002 if !supported {
5003 assert_eq!(load, CacheLoad::Unsupported);
5004 assert_eq!(renderer.save_pipeline_cache(), Ok(None));
5005 return;
5006 }
5007 assert_eq!(load, CacheLoad::Fresh);
5008 renderer.warm_opaque(frame::OpaqueFeatures::NONE, false);
5009 let saved = renderer.save_pipeline_cache().unwrap().unwrap();
5010 assert!(saved > 0);
5011 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5012 let mut again = Renderer::new(gpu, 32, 32).unwrap();
5013 assert_eq!(
5014 again.use_pipeline_cache(&path),
5015 CacheLoad::Loaded { bytes: saved }
5016 );
5017 again.warm_opaque(frame::OpaqueFeatures::NONE, false);
5018 let made = again.frame().opaque_made_on_render_thread();
5019 let pipelines = again.frame().opaque_pipelines().len();
5020 assert_eq!(made, pipelines);
5021 assert!(pipelines > 0);
5022 again.warm_opaque(frame::OpaqueFeatures::NONE, false);
5023 assert_eq!(again.frame().opaque_made_on_render_thread(), made);
5024 assert_eq!(again.frame().opaque_pipelines().len(), pipelines);
5025 let second = again.save_pipeline_cache().unwrap().unwrap();
5026 assert!(second > 0);
5027 assert_eq!(again.save_pipeline_cache().unwrap().unwrap(), second);
5028 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5029 let mut third = Renderer::new(gpu, 32, 32).unwrap();
5030 assert_eq!(
5031 third.use_pipeline_cache(&path),
5032 CacheLoad::Loaded { bytes: second }
5033 );
5034 }
5035
5036 #[test]
5037 #[ignore = "needs a GPU; run under pgpu"]
5038 fn an_invalid_pipeline_cache_is_ignored_and_replaced() {
5039 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5040 if !caches(&gpu) {
5041 return;
5042 }
5043 let key = crate::pipeline_cache::cache_key(&gpu.info).unwrap();
5044 let (mut renderer, target, instances, materials) = sphere_still(32);
5045 for (name, bytes) in [
5046 ("garbage.bin", b"not a cache at all".to_vec()),
5047 (
5048 "foreign.bin",
5049 crate::pipeline_cache::wrap(&key.replace("pito-engine", "older engine"), b"x"),
5050 ),
5051 ("damaged.bin", {
5052 let mut bytes = crate::pipeline_cache::wrap(&key, b"pipelines");
5053 let last = bytes.len() - 1;
5054 bytes[last] ^= 0x40;
5055 bytes
5056 }),
5057 ] {
5058 let path = cache_path(name);
5059 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
5060 std::fs::write(&path, bytes).unwrap();
5061 assert!(
5062 matches!(renderer.use_pipeline_cache(&path), CacheLoad::Ignored(_)),
5063 "{name} was not ignored"
5064 );
5065 }
5066 draw_frames(&mut renderer, &target, &instances, &materials, 1);
5067 let saved = renderer.save_pipeline_cache().unwrap().unwrap();
5068 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
5069 .join("../../tmp/pipeline-cache-tests/damaged.bin");
5070 assert_eq!(
5071 renderer.use_pipeline_cache(&path),
5072 CacheLoad::Loaded { bytes: saved }
5073 );
5074 draw_frames(&mut renderer, &target, &instances, &materials, 1);
5075 }
5076
5077 #[test]
5078 #[ignore = "needs a GPU; run under pgpu"]
5079 fn submitted_frames_match_blocking_frames_and_timings_arrive_late() {
5080 let frames = 120;
5081 let (mut blocking, blocking_target, instances, materials) = sphere_still(96);
5082 let mut timed = 0;
5083 for number in 0..frames {
5084 let timings = blocking
5085 .render(
5086 &still_scene(&instances, &materials, number),
5087 &Text::default(),
5088 &Effects::default(),
5089 Finish::Standard,
5090 &blocking_target.view,
5091 )
5092 .unwrap();
5093 if !timings.is_empty() {
5094 timed += 1;
5095 assert_eq!(blocking.timings().unwrap().frame, u64::from(number));
5096 assert_eq!(blocking.timings().unwrap().passes, timings);
5097 }
5098 }
5099 let expected = blocking.gpu().readback_rgba16(&blocking_target).unwrap();
5100 let (mut renderer, target, instances, materials) = sphere_still(96);
5101 let mut arrived = std::collections::BTreeMap::new();
5102 let mut lags = [0u32; 3];
5103 let mut most_in_flight = 0;
5104 for number in 0..frames {
5105 let submitted = renderer
5106 .submit(
5107 &still_scene(&instances, &materials, number),
5108 &Text::default(),
5109 &Effects::default(),
5110 Finish::Standard,
5111 &target.view,
5112 )
5113 .unwrap();
5114 assert_eq!(submitted.frame, u64::from(number));
5115 most_in_flight = most_in_flight.max(renderer.frames_in_flight());
5116 for timings in submitted.arrived {
5117 let lag = submitted.frame - timings.frame;
5118 assert!(
5119 lag <= TIMING_LAG,
5120 "frame {} arrived {lag} late",
5121 timings.frame
5122 );
5123 lags[lag as usize] += 1;
5124 assert!(!timings.passes.is_empty());
5125 assert!(arrived.insert(timings.frame, timings).is_none());
5126 }
5127 if let Some(latest) = renderer.timings() {
5128 assert_eq!(Some(&latest.frame), arrived.keys().last());
5129 }
5130 }
5131 for timings in renderer.wait_frames(0).unwrap() {
5132 assert!(u64::from(frames - 1) - timings.frame <= TIMING_LAG);
5133 assert!(arrived.insert(timings.frame, timings).is_none());
5134 }
5135 assert_eq!(renderer.frames_in_flight(), 0);
5136 let drops = renderer.timing_drops();
5137 println!(
5138 "{frames} submitted frames: {} timings arrived (lag 0: {}, 1: {}, 2: {}), dropped {} with the ring full and {} late; at most {most_in_flight} frames in flight; {timed} blocking frames timed",
5139 arrived.len(),
5140 lags[0],
5141 lags[1],
5142 lags[2],
5143 drops.ring,
5144 drops.late
5145 );
5146 if timed > 0 {
5147 assert_eq!(
5148 arrived.len() as u64 + drops.ring + drops.late,
5149 u64::from(frames)
5150 );
5151 assert_eq!(
5152 renderer.timings().map(|latest| latest.frame),
5153 arrived.keys().last().copied()
5154 );
5155 }
5156 let actual = renderer.gpu().readback_rgba16(&target).unwrap();
5157 assert!(
5158 actual == expected,
5159 "submitted frames differ from rendered ones"
5160 );
5161 if timed == 0 {
5162 return;
5163 }
5164 let mut paced = Vec::new();
5165 for number in frames..frames + 60 {
5166 let submitted = renderer
5167 .submit(
5168 &still_scene(&instances, &materials, number),
5169 &Text::default(),
5170 &Effects::default(),
5171 Finish::Standard,
5172 &target.view,
5173 )
5174 .unwrap();
5175 for timings in &submitted.arrived {
5176 assert!(submitted.frame - timings.frame <= TIMING_LAG);
5177 }
5178 paced.extend(submitted.arrived);
5179 for timings in renderer.wait_frames(2).unwrap() {
5180 assert!(submitted.frame - timings.frame <= TIMING_LAG);
5181 paced.push(timings);
5182 }
5183 assert!(renderer.frames_in_flight() <= 2);
5184 }
5185 paced.extend(renderer.wait_frames(0).unwrap());
5186 println!(
5187 "60 frames paced to two in flight: {} timings arrived, drops {:?}",
5188 paced.len(),
5189 renderer.timing_drops()
5190 );
5191 assert_eq!(
5192 paced
5193 .iter()
5194 .map(|timings| timings.frame)
5195 .collect::<Vec<_>>(),
5196 (u64::from(frames)..u64::from(frames) + 60).collect::<Vec<_>>()
5197 );
5198 assert_eq!(renderer.timing_drops(), drops);
5199 }
5200
5201 #[test]
5202 #[ignore = "needs a GPU; run under pgpu"]
5203 fn a_scene_without_a_volume_is_the_same_bytes_under_any_volume_cuts() {
5204 let mut shots = Vec::new();
5205 for cuts in [
5206 effects::VolumeCuts::default(),
5207 effects::VolumeCuts::FULL,
5208 effects::VolumeCuts {
5209 steps: 6,
5210 warp_octaves: 1,
5211 octaves: 1,
5212 early_out: true,
5213 },
5214 ] {
5215 let (mut renderer, target, instances, materials) = sphere_still(96);
5216 renderer.set_volume_cuts(cuts);
5217 assert_eq!(renderer.volume_cuts(), cuts);
5218 for number in 0..6 {
5219 renderer
5220 .render(
5221 &still_scene(&instances, &materials, number),
5222 &Text::default(),
5223 &Effects::default(),
5224 Finish::Standard,
5225 &target.view,
5226 )
5227 .unwrap();
5228 }
5229 assert!(!renderer.last_pass_order().contains(&"volume march"));
5230 shots.push(renderer.gpu().readback_rgba16(&target).unwrap());
5231 }
5232 assert!(shots[0] == shots[1] && shots[1] == shots[2]);
5233 }
5234
5235 #[test]
5236 #[ignore = "needs a GPU; run under pgpu"]
5237 fn a_stepped_sky_swaps_in_whole_and_renders_as_the_settled_one() {
5238 let old = AnalyticSky::new(
5239 pfx_core::daylight::Daylight {
5240 hour: 9.0,
5241 day: 172.0,
5242 latitude: 45.0,
5243 heading: 180.0,
5244 },
5245 pfx_core::daylight::REFERENCE_HOUR,
5246 2.5,
5247 [0.2, 0.25, 0.2],
5248 );
5249 let sky = |hour: f64| {
5250 SkySource::Analytic(AnalyticSky::new(
5251 pfx_core::daylight::Daylight {
5252 hour,
5253 day: 172.0,
5254 latitude: 45.0,
5255 heading: 180.0,
5256 },
5257 pfx_core::daylight::REFERENCE_HOUR,
5258 2.5,
5259 [0.2, 0.25, 0.2],
5260 ))
5261 };
5262 let sh_bits = |r: &Renderer| {
5263 r.frame()
5264 .sky()
5265 .lighting
5266 .sh
5267 .map(|band| band.map(f32::to_bits))
5268 };
5269 let (mut settled, settled_target, instances, materials) = sphere_still(64);
5270 settled.set_sky(SkySource::Analytic(old), 9.0).unwrap();
5271 let blocking = std::time::Instant::now();
5272 settled.set_sky(sky(15.0), 15.0).unwrap();
5273 let blocking_ms = blocking.elapsed().as_secs_f64() * 1000.0;
5274 let (mut stepped, stepped_target, _, _) = sphere_still(64);
5275 stepped.set_sky(SkySource::Analytic(old), 9.0).unwrap();
5276 let before = sh_bits(&stepped);
5277 let mut calls = Vec::new();
5278 let mut last = 0;
5279 for index in 0..20 {
5280 let started = std::time::Instant::now();
5281 last = stepped
5282 .request_sky(sky(10.0 + index as f64 * 0.25), 10.0 + index as f32 * 0.25)
5283 .unwrap();
5284 calls.push(started.elapsed().as_secs_f64() * 1000.0);
5285 }
5286 let started = std::time::Instant::now();
5287 let newest = stepped.request_sky(sky(15.0), 15.0).unwrap();
5288 calls.push(started.elapsed().as_secs_f64() * 1000.0);
5289 assert!(newest > last);
5290 assert!(stepped.sky_settling());
5291 let after = sh_bits(&settled);
5292 let mut number = 0;
5293 let mut whole = true;
5294 let settling = std::time::Instant::now();
5295 while stepped.sky_settling() && number < 600 {
5296 stepped
5297 .render(
5298 &still_scene(&instances, &materials, number),
5299 &Text::default(),
5300 &Effects::default(),
5301 Finish::Standard,
5302 &stepped_target.view,
5303 )
5304 .unwrap();
5305 let now = sh_bits(&stepped);
5306 whole &= now == before || now == after;
5307 number += 1;
5308 }
5309 if stepped.sky_settling() {
5310 stepped.wait_sky().unwrap();
5311 }
5312 let settled_ms = settling.elapsed().as_secs_f64() * 1000.0;
5313 assert!(
5314 whole,
5315 "a frame showed a sky that was neither the old nor the new one"
5316 );
5317 assert_eq!(sh_bits(&stepped), after);
5318 assert_eq!(stepped.frame().sky().shown(), newest);
5319 assert_eq!(stepped.hour, 15.0);
5320 for number in 1000..1008 {
5321 for (renderer, target) in [
5322 (&mut settled, &settled_target),
5323 (&mut stepped, &stepped_target),
5324 ] {
5325 renderer
5326 .render(
5327 &still_scene(&instances, &materials, number),
5328 &Text::default(),
5329 &Effects::default(),
5330 Finish::Standard,
5331 &target.view,
5332 )
5333 .unwrap();
5334 }
5335 }
5336 let mean = calls.iter().sum::<f64>() / calls.len() as f64;
5337 let most = calls.iter().copied().fold(0.0, f64::max);
5338 println!(
5339 "request_sky on the calling thread: mean {mean:.4} ms, max {most:.4} ms over {} calls; the new sky swapped in after {number} frames and {settled_ms:.1} ms; set_sky with settle blocked {blocking_ms:.1} ms",
5340 calls.len()
5341 );
5342 assert!(mean < 1.0);
5343 assert!(
5344 settled.gpu().readback_rgba16(&settled_target).unwrap()
5345 == stepped.gpu().readback_rgba16(&stepped_target).unwrap(),
5346 "the stepped sky renders differently from the settled one"
5347 );
5348 }
5349
5350 #[test]
5351 #[ignore = "needs a GPU; run under pgpu"]
5352 fn pick_returns_id_and_depth_after_submission() {
5353 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5354 let mut renderer = Renderer::new(gpu, 64, 64).unwrap();
5355 renderer.shadows = Shadows::new(
5356 &renderer.gpu().device,
5357 Quality {
5358 resolution: 128,
5359 ..Default::default()
5360 },
5361 );
5362 let shape = Shape::RoundBox {
5363 half: [0.8; 3],
5364 radius: 0.05,
5365 }
5366 .mesh(0.2);
5367 let mesh = renderer
5368 .upload_mesh(MeshData {
5369 positions: &shape.positions,
5370 normals: &shape.normals,
5371 tangents: &shape.tangents,
5372 uvs: &shape.uvs,
5373 uvs1: None,
5374 alpha: None,
5375 indices: &shape.indices,
5376 })
5377 .unwrap();
5378 let instances = [Instance::new(mesh, identity(), 0, 37)];
5379 let mut view = identity();
5380 view[3][2] = -4.0;
5381 let projection = perspective(1.0);
5382 let camera = Camera {
5383 view,
5384 projection,
5385 previous_view_projection: frame::multiply(projection, view),
5386 position: [0.0, 0.0, 4.0],
5387 };
5388 let materials = [Material::default()];
5389 let target = output(renderer.gpu(), 64, 64);
5390 renderer.pick(32, 32).unwrap();
5391 let mut picked = None;
5392 for frame_number in 0..20 {
5393 let scene = Scene {
5394 camera,
5395 time: frame_number as f32 / 60.0,
5396 seed: 3,
5397 sun: Sun {
5398 direction: [0.0, 1.0, 0.0],
5399 colour: [1.0; 3],
5400 intensity: 2.0,
5401 },
5402 instances: &instances,
5403 materials: &materials,
5404 deformers: &[],
5405 wind: SceneWind::default(),
5406 };
5407 renderer
5408 .render(
5409 &scene,
5410 &Text::default(),
5411 &Effects::default(),
5412 Finish::Standard,
5413 &target.view,
5414 )
5415 .unwrap();
5416 picked = renderer.picked();
5417 if picked.is_some() {
5418 break;
5419 }
5420 }
5421 let picked = picked.expect("pick did not complete within twenty submitted frames");
5422 assert_eq!((picked.x, picked.y, picked.id), (32, 32, 37));
5423 assert!(picked.depth.is_finite() && picked.depth > 0.0 && picked.depth < 100.0);
5424 }
5425
5426 #[test]
5427 #[ignore = "needs a GPU; run under pgpu"]
5428 fn creature_depth_reaches_atlas_after_renderer_render() {
5429 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5430 let mut renderer = Renderer::new(gpu, 64, 64).unwrap();
5431 renderer.set_shadow_quality(Quality {
5432 resolution: 64,
5433 receiver: Some(crate::shadow::ReceiverBox {
5434 min: [-1.0, -0.1, -1.0],
5435 max: [1.0, 2.0, 1.0],
5436 }),
5437 caster_margin: 2.0,
5438 ..Quality::default()
5439 });
5440 let mesh = CreatureMesh::new(
5441 &renderer.gpu().device,
5442 &[
5443 effects::CreatureVertex {
5444 position: [-0.4, 0.0, -0.4, 0.0],
5445 },
5446 effects::CreatureVertex {
5447 position: [0.4, 0.0, -0.4, 0.0],
5448 },
5449 effects::CreatureVertex {
5450 position: [0.0, 0.0, 0.4, 0.0],
5451 },
5452 ],
5453 &[0, 1, 2],
5454 );
5455 let bird = [CreatureInstance {
5456 position_scale: [0.0, 1.0, 0.0, 1.0],
5457 right: [1.0, 0.0, 0.0, 0.0],
5458 up: [0.0, 1.0, 0.0, 0.0],
5459 forward: [0.0, 0.0, 1.0, 0.0],
5460 motion: [0.0; 4],
5461 }];
5462 let mut view = identity();
5463 view[3][2] = -4.0;
5464 let projection = perspective(1.0);
5465 let scene = Scene {
5466 camera: Camera {
5467 view,
5468 projection,
5469 previous_view_projection: frame::multiply(projection, view),
5470 position: [0.0, 0.0, 4.0],
5471 },
5472 time: 0.0,
5473 seed: 5,
5474 sun: Sun {
5475 direction: [0.0, 1.0, 0.0],
5476 colour: [1.0; 3],
5477 intensity: 2.0,
5478 },
5479 instances: &[],
5480 materials: &[Material::default()],
5481 deformers: &[],
5482 wind: SceneWind::default(),
5483 };
5484 let output = output(renderer.gpu(), 64, 64);
5485 renderer
5486 .render(
5487 &scene,
5488 &Text::default(),
5489 &Effects {
5490 creatures: &bird,
5491 creature_mesh: Some(&mesh),
5492 ..Default::default()
5493 },
5494 Finish::Standard,
5495 &output.view,
5496 )
5497 .unwrap();
5498 assert!(renderer.last_pass_order().contains(&"creature shadows"));
5499 let readback = renderer
5500 .gpu()
5501 .device
5502 .create_buffer(&wgpu::BufferDescriptor {
5503 label: Some("creature shadow readback"),
5504 size: 64 * 64 * 4 * crate::shadow::CASCADE_COUNT as u64,
5505 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5506 mapped_at_creation: false,
5507 });
5508 let mut encoder = renderer
5509 .gpu()
5510 .device
5511 .create_command_encoder(&Default::default());
5512 encoder.copy_texture_to_buffer(
5513 wgpu::TexelCopyTextureInfo {
5514 texture: renderer.shadows().atlas(),
5515 mip_level: 0,
5516 origin: wgpu::Origin3d::ZERO,
5517 aspect: wgpu::TextureAspect::DepthOnly,
5518 },
5519 wgpu::TexelCopyBufferInfo {
5520 buffer: &readback,
5521 layout: wgpu::TexelCopyBufferLayout {
5522 offset: 0,
5523 bytes_per_row: Some(256),
5524 rows_per_image: Some(64),
5525 },
5526 },
5527 wgpu::Extent3d {
5528 width: 64,
5529 height: 64,
5530 depth_or_array_layers: crate::shadow::CASCADE_COUNT as u32,
5531 },
5532 );
5533 renderer.gpu().queue.submit(Some(encoder.finish()));
5534 let (send, receive) = std::sync::mpsc::channel();
5535 readback
5536 .slice(..)
5537 .map_async(wgpu::MapMode::Read, move |result| {
5538 send.send(result).unwrap();
5539 });
5540 renderer
5541 .gpu()
5542 .device
5543 .poll(wgpu::PollType::wait_indefinitely())
5544 .unwrap();
5545 receive.recv().unwrap().unwrap();
5546 let bytes = readback
5547 .slice(..)
5548 .get_mapped_range()
5549 .expect("a mapped range");
5550 assert!(
5551 bytes
5552 .chunks_exact(4)
5553 .any(|pixel| { f32::from_le_bytes(pixel.try_into().unwrap()) < 0.999 })
5554 );
5555 }
5556
5557 #[test]
5558 #[ignore = "needs a GPU; run under pgpu"]
5559 fn prop_shadow_reaches_cascade_atlas_after_renderer_render() {
5560 use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
5561 use pfx_bake::{Anchor, BakeScene};
5562 use pfx_load::Sky;
5563
5564 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5565 let shape = Shape::RoundBox {
5566 half: [0.5; 3],
5567 radius: 0.04,
5568 }
5569 .mesh(0.03);
5570 let triangles = triangles(&shape, [0.0; 3], 0);
5571 let bounds = bounds(&triangles, &[]).unwrap();
5572 let sky = Sky {
5573 width: 4,
5574 height: 2,
5575 texels: vec![[0.5; 4]; 8],
5576 };
5577 let baked = BakeScene {
5578 triangles,
5579 shapes: Vec::new(),
5580 materials: vec![Material::default()],
5581 anchors: vec![Anchor {
5582 hour: 12.0,
5583 sky: sky.clone(),
5584 sun: pfx_trace::Sun {
5585 direction: [0.0, 1.0, 0.0],
5586 color: [1.0; 3],
5587 intensity: 2.0,
5588 },
5589 }],
5590 };
5591 let artifact = bake_prop(
5592 &gpu,
5593 &baked,
5594 &PropSpec {
5595 name: "shadow-test".into(),
5596 node: Some("box".into()),
5597 file: None,
5598 frames_per_side: 8,
5599 atlas_size: 256,
5600 hemisphere: false,
5601 anchors: vec![12.0],
5602 },
5603 bounds,
5604 1,
5605 5,
5606 )
5607 .unwrap();
5608 let mut renderer = Renderer::new(gpu, 160, 160).unwrap();
5609 renderer.shadows = Shadows::new(
5610 &renderer.gpu().device,
5611 Quality {
5612 resolution: 128,
5613 ..Default::default()
5614 },
5615 );
5616 renderer.set_sky(SkySource::Hdr(sky), 12.0).unwrap();
5617 let props = ImpostorPass::new(
5618 renderer.gpu(),
5619 artifact,
5620 wgpu::TextureFormat::Rgba16Float,
5621 wgpu::TextureFormat::Depth32Float,
5622 1,
5623 )
5624 .unwrap();
5625 renderer.set_props(Some(props));
5626 let output = output(renderer.gpu(), 160, 160);
5627 let mut view = identity();
5628 view[3][2] = -4.0;
5629 let projection = perspective(1.0);
5630 let camera = Camera {
5631 view,
5632 projection,
5633 previous_view_projection: frame::multiply(projection, view),
5634 position: [0.0, 0.0, 4.0],
5635 };
5636 let materials = [Material::default()];
5637 let scene = Scene {
5638 camera,
5639 time: 0.0,
5640 seed: 5,
5641 sun: Sun {
5642 direction: [0.0, 1.0, 0.0],
5643 colour: [1.0; 3],
5644 intensity: 2.0,
5645 },
5646 instances: &[],
5647 materials: &materials,
5648 deformers: &[],
5649 wind: SceneWind::default(),
5650 };
5651 let item = [ImpostorInstance::new(bounds.center, bounds.radius, 1, 0.0).unwrap()];
5652 renderer
5653 .render(
5654 &scene,
5655 &Text::default(),
5656 &Effects {
5657 props: &item,
5658 ..Default::default()
5659 },
5660 Finish::Standard,
5661 &output.view,
5662 )
5663 .unwrap();
5664 assert!(renderer.last_pass_order().contains(&"impostor shadows"));
5665 let read_mask = |renderer: &Renderer| {
5666 let size = 128u32;
5667 let buffer = renderer
5668 .gpu()
5669 .device
5670 .create_buffer(&wgpu::BufferDescriptor {
5671 label: Some("renderer shadow readback"),
5672 size: u64::from(size * size * 4 * crate::shadow::CASCADE_COUNT as u32),
5673 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5674 mapped_at_creation: false,
5675 });
5676 let mut encoder = renderer
5677 .gpu()
5678 .device
5679 .create_command_encoder(&Default::default());
5680 encoder.copy_texture_to_buffer(
5681 wgpu::TexelCopyTextureInfo {
5682 texture: renderer.shadows.atlas(),
5683 mip_level: 0,
5684 origin: wgpu::Origin3d::ZERO,
5685 aspect: wgpu::TextureAspect::DepthOnly,
5686 },
5687 wgpu::TexelCopyBufferInfo {
5688 buffer: &buffer,
5689 layout: wgpu::TexelCopyBufferLayout {
5690 offset: 0,
5691 bytes_per_row: Some(size * 4),
5692 rows_per_image: Some(size),
5693 },
5694 },
5695 wgpu::Extent3d {
5696 width: size,
5697 height: size,
5698 depth_or_array_layers: crate::shadow::CASCADE_COUNT as u32,
5699 },
5700 );
5701 renderer.gpu().queue.submit(Some(encoder.finish()));
5702 let (send, receive) = std::sync::mpsc::channel();
5703 buffer
5704 .slice(..)
5705 .map_async(wgpu::MapMode::Read, move |result| {
5706 send.send(result).unwrap();
5707 });
5708 renderer
5709 .gpu()
5710 .device
5711 .poll(wgpu::PollType::wait_indefinitely())
5712 .unwrap();
5713 receive.recv().unwrap().unwrap();
5714 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
5715 let mask = mapped
5716 .chunks_exact(4)
5717 .map(|bytes| f32::from_le_bytes(bytes.try_into().unwrap()) < 0.999)
5718 .collect::<Vec<_>>();
5719 drop(mapped);
5720 buffer.unmap();
5721 mask
5722 };
5723 let full = read_mask(&renderer);
5724 assert!(full.iter().any(|&covered| covered));
5725 let mesh = renderer
5726 .upload_mesh(MeshData {
5727 positions: &shape.positions,
5728 normals: &shape.normals,
5729 tangents: &shape.tangents,
5730 uvs: &shape.uvs,
5731 uvs1: None,
5732 alpha: None,
5733 indices: &shape.indices,
5734 })
5735 .unwrap();
5736 let mut mesh_instance = Instance::new(mesh, identity(), 0, 1);
5737 mesh_instance.coverage = 0.5;
5738 let instances = [mesh_instance];
5739 let mixed_scene = Scene {
5740 time: 1.0 / 60.0,
5741 instances: &instances,
5742 ..scene
5743 };
5744 let mixed_prop = [ImpostorInstance::new(bounds.center, bounds.radius, 1, 0.5).unwrap()];
5745 renderer
5746 .render(
5747 &mixed_scene,
5748 &Text::default(),
5749 &Effects {
5750 props: &mixed_prop,
5751 ..Default::default()
5752 },
5753 Finish::Standard,
5754 &output.view,
5755 )
5756 .unwrap();
5757 let mixed = read_mask(&renderer);
5758 let overlap = full.iter().zip(&mixed).filter(|(a, b)| **a && **b).count();
5759 let union = full.iter().zip(&mixed).filter(|(a, b)| **a || **b).count();
5760 assert!(
5761 union > 0 && overlap * 10 >= union * 7,
5762 "crossfade shadow footprint {overlap}/{union}"
5763 );
5764 }
5765
5766 #[test]
5767 fn history_resets_on_a_cut() {
5768 let a = perspective(1.5);
5769 let mut b = a;
5770 b[3][0] = 0.4;
5771 let frame = |n: f32| n / 60.0;
5772 let mut history = History::default();
5773 assert!(!history.advance(frame(0.0), a, a));
5774 assert!(history.advance(frame(1.0), a, a));
5775 assert!(history.advance(frame(2.0), a, b));
5776 assert!(history.advance(frame(3.0), b, b));
5777 assert!(
5778 !history.advance(frame(4.0), a, a),
5779 "a camera that does not chain is a cut"
5780 );
5781 assert!(history.advance(frame(5.0), a, a));
5782 assert!(
5783 !history.advance(frame(30.0), a, a),
5784 "a jump in time is a cut"
5785 );
5786 assert!(history.advance(frame(31.0), a, a));
5787 history.invalidate();
5788 assert!(
5789 !history.advance(frame(32.0), a, a),
5790 "a resize or daylight jump resets"
5791 );
5792 assert!(history.advance(frame(33.0), a, a));
5793 assert_eq!(history.previous_time(), Some(frame(33.0)));
5794 }
5795
5796 #[test]
5797 fn history_follows_a_scripted_clock() {
5798 let a = perspective(1.5);
5799 let mut history = History::default();
5800 assert!(!history.advance(0.0, a, a));
5801 for step in 1..=30 {
5802 assert!(
5803 history.advance(step as f32 / 30.0, a, a),
5804 "a 30 fps clip keeps its history"
5805 );
5806 }
5807 assert!(history.advance(1.0, a, a), "a paused clock keeps it");
5808 assert!(
5809 history.advance(1.0 + 4.0 / 30.0, a, a),
5810 "a fast clip keeps it"
5811 );
5812 assert!(
5813 history.advance(1.2, a, a),
5814 "a dropped window frame keeps it"
5815 );
5816 assert!(!history.advance(1.1, a, a), "time going back is a cut");
5817 history.set_max_gap(0.05);
5818 assert!(
5819 !history.advance(1.2, a, a),
5820 "a gap over the setting is a cut"
5821 );
5822 assert!(history.advance(1.25, a, a));
5823 history.set_max_gap(f32::NAN);
5824 assert_eq!(history.max_gap(), HISTORY_GAP);
5825 }
5826
5827 #[test]
5828 fn inverse_and_fullscreen_shaders() {
5829 let mut view = identity();
5830 view[3] = [0.3, -0.2, -4.0, 1.0];
5831 let matrix = frame::multiply(perspective(1.7), view);
5832 let inverse = invert(matrix).unwrap();
5833 let product = frame::multiply(matrix, inverse);
5834 for (c, column) in product.iter().enumerate() {
5835 for (r, value) in column.iter().enumerate() {
5836 let expected = if c == r { 1.0 } else { 0.0 };
5837 assert!((value - expected).abs() < 1e-4, "{product:?}");
5838 }
5839 }
5840 assert!(invert([[0.0; 4]; 4]).is_none());
5841 for source in [motion_source(), downsample_source()] {
5842 let module = naga::front::wgsl::parse_str(&source).unwrap();
5843 naga::valid::Validator::new(
5844 naga::valid::ValidationFlags::all(),
5845 naga::valid::Capabilities::all(),
5846 )
5847 .validate(&module)
5848 .unwrap();
5849 }
5850 }
5851
5852 fn output(gpu: &Gpu, width: u32, height: u32) -> OffscreenTarget {
5853 let format = wgpu::TextureFormat::Rgba16Float;
5854 let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
5855 label: Some("prop output"),
5856 size: wgpu::Extent3d {
5857 width,
5858 height,
5859 depth_or_array_layers: 1,
5860 },
5861 mip_level_count: 1,
5862 sample_count: 1,
5863 dimension: wgpu::TextureDimension::D2,
5864 format,
5865 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5866 | wgpu::TextureUsages::TEXTURE_BINDING
5867 | wgpu::TextureUsages::STORAGE_BINDING
5868 | wgpu::TextureUsages::COPY_SRC,
5869 view_formats: &[],
5870 });
5871 let view = texture.create_view(&Default::default());
5872 OffscreenTarget {
5873 texture,
5874 view,
5875 format,
5876 width,
5877 height,
5878 }
5879 }
5880
5881 fn triangles(mesh: &Mesh, offset: [f32; 3], material: u32) -> Vec<pfx_trace::bvh::Triangle> {
5882 mesh.indices
5883 .chunks_exact(3)
5884 .map(|tri| pfx_trace::bvh::Triangle {
5885 vertices: [tri[0], tri[1], tri[2]].map(|index| {
5886 [0, 1, 2].map(|axis| mesh.positions[index as usize][axis] + offset[axis])
5887 }),
5888 material,
5889 })
5890 .collect()
5891 }
5892
5893 #[test]
5894 #[ignore = "needs a GPU; run under pgpu"]
5895 fn prop_matches_its_mesh_under_the_sky() {
5896 use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
5897 use pfx_bake::{Anchor, BakeScene};
5898 use pfx_load::Sky;
5899 let width = 3840;
5900 let height = 2160;
5901 let gpu = pollster::block_on(Gpu::headless()).unwrap();
5902 let box_mesh = Shape::RoundBox {
5903 half: [0.8, 0.4, 0.6],
5904 radius: 0.12,
5905 }
5906 .mesh(0.004);
5907 let sphere_mesh = Shape::Ellipsoid {
5908 radius: 0.24,
5909 squash: 1.0,
5910 }
5911 .mesh(0.004);
5912 let sphere_offset = [0.0, 0.62, 0.0];
5913 let mut tris = triangles(&box_mesh, [0.0; 3], 0);
5914 tris.extend(triangles(&sphere_mesh, sphere_offset, 1));
5915 let materials = vec![
5916 Material {
5917 base: [0.75, 0.16, 0.08],
5918 roughness: 0.7,
5919 ..Default::default()
5920 },
5921 Material {
5922 base: [0.9; 3],
5923 metalness: 1.0,
5924 roughness: 0.22,
5925 ..Default::default()
5926 },
5927 ];
5928 let sky = Sky {
5929 width: 4,
5930 height: 2,
5931 texels: vec![[0.3, 0.4, 0.6, 1.0]; 8],
5932 };
5933 let sun = [0.4, 0.8, 0.4];
5934 let scene = BakeScene {
5935 triangles: tris,
5936 shapes: Vec::new(),
5937 materials: materials.clone(),
5938 anchors: vec![Anchor {
5939 hour: 12.0,
5940 sky: sky.clone(),
5941 sun: pfx_trace::Sun {
5942 direction: sun,
5943 color: [1.0; 3],
5944 intensity: 2.0,
5945 },
5946 }],
5947 };
5948 let prop_bounds = bounds(&scene.triangles, &scene.shapes).unwrap();
5949 let spec = PropSpec {
5950 name: "rounded-box-chrome".into(),
5951 node: Some("test".into()),
5952 file: None,
5953 frames_per_side: 16,
5954 atlas_size: 2048,
5955 hemisphere: false,
5956 anchors: vec![12.0],
5957 };
5958 let artifact = bake_prop(&gpu, &scene, &spec, prop_bounds, 512, 7).unwrap();
5959 let mut renderer = Renderer::new(gpu, width, height).unwrap();
5960 renderer.set_sky(SkySource::Hdr(sky), 12.0).unwrap();
5961 let props = ImpostorPass::new(
5962 renderer.gpu(),
5963 artifact,
5964 wgpu::TextureFormat::Rgba16Float,
5965 wgpu::TextureFormat::Depth32Float,
5966 4,
5967 )
5968 .unwrap();
5969 renderer.set_props(Some(props));
5970 let box_handle = renderer
5971 .upload_mesh(MeshData {
5972 positions: &box_mesh.positions,
5973 normals: &box_mesh.normals,
5974 tangents: &box_mesh.tangents,
5975 uvs: &box_mesh.uvs,
5976 uvs1: None,
5977 alpha: None,
5978 indices: &box_mesh.indices,
5979 })
5980 .unwrap();
5981 let sphere_handle = renderer
5982 .upload_mesh(MeshData {
5983 positions: &sphere_mesh.positions,
5984 normals: &sphere_mesh.normals,
5985 tangents: &sphere_mesh.tangents,
5986 uvs: &sphere_mesh.uvs,
5987 uvs1: None,
5988 alpha: None,
5989 indices: &sphere_mesh.indices,
5990 })
5991 .unwrap();
5992 let mut sphere_model = identity();
5993 sphere_model[3] = [0.0, sphere_offset[1], 0.0, 1.0];
5994 let meshes = [
5995 Instance::new(box_handle, identity(), 0, 1),
5996 Instance::new(sphere_handle, sphere_model, 1, 2),
5997 ];
5998 let card = [ImpostorInstance::new(prop_bounds.center, prop_bounds.radius, 3, 0.0).unwrap()];
5999 let look = |eye: [f32; 3]| {
6000 let d = [
6001 prop_bounds.center[0] - eye[0],
6002 prop_bounds.center[1] - eye[1],
6003 prop_bounds.center[2] - eye[2],
6004 ];
6005 let l = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
6006 let forward = d.map(|v| v / l);
6007 let r = [-forward[2], 0.0, forward[0]];
6008 let rl = (r[0] * r[0] + r[2] * r[2]).sqrt();
6009 let right = r.map(|v| v / rl);
6010 let up = [
6011 right[1] * forward[2] - right[2] * forward[1],
6012 right[2] * forward[0] - right[0] * forward[2],
6013 right[0] * forward[1] - right[1] * forward[0],
6014 ];
6015 let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6016 let view = [
6017 [right[0], up[0], -forward[0], 0.0],
6018 [right[1], up[1], -forward[1], 0.0],
6019 [right[2], up[2], -forward[2], 0.0],
6020 [-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
6021 ];
6022 let projection = perspective(width as f32 / height as f32);
6023 Camera {
6024 view,
6025 projection,
6026 previous_view_projection: frame::multiply(projection, view),
6027 position: eye,
6028 }
6029 };
6030 let views = [
6031 ("head-on", look([0.0, prop_bounds.center[1], 20.0])),
6032 ("oblique", look([8.4, prop_bounds.center[1] + 5.3, 17.6])),
6033 ];
6034 let target = output(renderer.gpu(), width, height);
6035 let mut start = 0;
6036 let mut results = Vec::new();
6037 for (name, camera) in views {
6038 let mut capture =
6039 |renderer: &mut Renderer, instances: &[Instance], props: &[ImpostorInstance]| {
6040 for number in start..start + 16 {
6041 let scene = Scene {
6042 camera,
6043 time: number as f32 / 60.0,
6044 seed: 7,
6045 sun: Sun {
6046 direction: sun,
6047 colour: [1.0; 3],
6048 intensity: 2.0,
6049 },
6050 instances,
6051 materials: &materials,
6052 deformers: &[],
6053 wind: SceneWind::default(),
6054 };
6055 renderer
6056 .render(
6057 &scene,
6058 &Text::default(),
6059 &Effects {
6060 props,
6061 ..Default::default()
6062 },
6063 Finish::Standard,
6064 &target.view,
6065 )
6066 .unwrap();
6067 }
6068 start += 100;
6069 renderer
6070 .gpu()
6071 .readback_rgba16(&target)
6072 .unwrap()
6073 .chunks_exact(4)
6074 .map(|pixel| [0, 1, 2].map(|c| half::f16::from_bits(pixel[c]).to_f32()))
6075 .collect::<Vec<_>>()
6076 };
6077 let background = capture(&mut renderer, &[], &[]);
6078 let mesh = capture(&mut renderer, &meshes, &[]);
6079 let prop = capture(&mut renderer, &[], &card);
6080 let differs = |a: [f32; 3], b: [f32; 3]| (0..3).any(|c| (a[c] - b[c]).abs() > 0.004);
6081 let mask: Vec<bool> = (0..mesh.len())
6082 .map(|i| differs(mesh[i], background[i]) || differs(prop[i], background[i]))
6083 .collect();
6084 let clip = frame::transform(
6085 frame::multiply(camera.projection, camera.view),
6086 [sphere_offset[0], sphere_offset[1], sphere_offset[2], 1.0],
6087 );
6088 let centre = [
6089 (clip[0] / clip[3] * 0.5 + 0.5) * width as f32,
6090 (0.5 - clip[1] / clip[3] * 0.5) * height as f32,
6091 ];
6092 let reach = 0.24 * 1.15 * camera.projection[1][1] * height as f32 * 0.5 / clip[3];
6093 let on_sphere = |i: usize| {
6094 let x = (i % width as usize) as f32 + 0.5 - centre[0];
6095 let y = (i / width as usize) as f32 + 0.5 - centre[1];
6096 x * x + y * y <= reach * reach
6097 };
6098 let mean = |keep: &dyn Fn(usize) -> bool| {
6099 let picked: Vec<usize> = (0..mesh.len()).filter(|i| mask[*i] && keep(*i)).collect();
6100 let sum = picked
6101 .iter()
6102 .map(|&i| (0..3).map(|c| (mesh[i][c] - prop[i][c]).abs()).sum::<f32>())
6103 .sum::<f32>();
6104 (sum / (picked.len().max(1) * 3) as f32, picked.len())
6105 };
6106 let (error, count) = mean(&|_| true);
6107 let (box_error, box_count) = mean(&|i| !on_sphere(i));
6108 let (chrome_error, chrome_count) = mean(&|i| on_sphere(i));
6109 let both: Vec<usize> = (0..mesh.len())
6110 .filter(|&i| {
6111 !on_sphere(i)
6112 && differs(mesh[i], background[i])
6113 && differs(prop[i], background[i])
6114 })
6115 .collect();
6116 let bias = [0, 1, 2].map(|c| {
6117 both.iter().map(|&i| prop[i][c] - mesh[i][c]).sum::<f32>()
6118 / both.len().max(1) as f32
6119 });
6120 let interior = both
6121 .iter()
6122 .map(|&i| (0..3).map(|c| (mesh[i][c] - prop[i][c]).abs()).sum::<f32>())
6123 .sum::<f32>()
6124 / (both.len().max(1) * 3) as f32;
6125 println!(
6126 "{name}: box bias where both cover {bias:?}, MAE {interior:.4} over {} pixels",
6127 both.len()
6128 );
6129 println!(
6130 "{name}: rounded box MAE {box_error:.4} over {box_count} pixels, chrome sphere MAE {chrome_error:.4} over {chrome_count} pixels"
6131 );
6132 let covered = |image: &[[f32; 3]]| -> Vec<bool> {
6133 (0..image.len())
6134 .map(|i| differs(image[i], background[i]))
6135 .collect()
6136 };
6137 let speckles = |cover: &[bool]| {
6138 let w = width as usize;
6139 let h = height as usize;
6140 let mut lonely = 0;
6141 for y in 1..h - 1 {
6142 for x in 1..w - 1 {
6143 let here = cover[y * w + x];
6144 let same = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)]
6145 .iter()
6146 .filter(|(dx, dy)| {
6147 cover[(y as i32 + dy) as usize * w + (x as i32 + dx) as usize]
6148 == here
6149 })
6150 .count();
6151 if same == 0 {
6152 lonely += 1;
6153 }
6154 }
6155 }
6156 lonely
6157 };
6158 let mesh_speckles = speckles(&covered(&mesh));
6159 let prop_speckles = speckles(&covered(&prop));
6160 println!(
6161 "{name}: object-only display MAE {error:.4} over {count} pixels; isolated edge pixels: mesh {mesh_speckles}, prop {prop_speckles}"
6162 );
6163 results.push((name, error, count, mesh_speckles, prop_speckles, interior));
6164 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
6165 std::fs::create_dir_all(&root).unwrap();
6166 let file =
6167 std::fs::File::create(root.join(format!("prop-against-mesh-{name}.png"))).unwrap();
6168 let mut encoder = png::Encoder::new(file, width * 2, height);
6169 encoder.set_color(png::ColorType::Rgb);
6170 encoder.set_depth(png::BitDepth::Eight);
6171 let mut bytes = Vec::with_capacity((width * 2 * height * 3) as usize);
6172 for y in 0..height as usize {
6173 for image in [&mesh, &prop] {
6174 for x in 0..width as usize {
6175 bytes.extend(
6176 image[y * width as usize + x]
6177 .map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8),
6178 );
6179 }
6180 }
6181 }
6182 encoder
6183 .write_header()
6184 .unwrap()
6185 .write_image_data(&bytes)
6186 .unwrap();
6187 }
6188 let (_, _, count, mesh_speckles, prop_speckles, _) = results[0];
6189 assert!(count > 20_000, "{count} object pixels");
6190 for (name, _, _, _, _, box_error) in &results {
6191 assert!(
6192 *box_error <= 0.02,
6193 "{name}: rounded box interior MAE {box_error:.4}"
6194 );
6195 }
6196 assert!(
6197 prop_speckles <= mesh_speckles + count / 2000,
6198 "speckled prop edges"
6199 );
6200 }
6201
6202 const PICK_SIZE: u32 = 128;
6203 const FLOOR_ID: u32 = 3;
6204 const PANEL_ID: u32 = 4;
6205 const GLASS_PIXEL: (u32, u32) = (27, 64);
6206 const TEXT_PIXEL: (u32, u32) = (99, 64);
6207 const PANEL_PIXEL: (u32, u32) = (121, 64);
6208 const OVERLAY_PIXEL: (u32, u32) = (64, 112);
6209 const FLOOR_PIXEL: (u32, u32) = (64, 20);
6210
6211 struct IdShot {
6212 colour: Vec<u16>,
6213 ids: Vec<u32>,
6214 picked: Picked,
6215 bounds: Vec<TextBounds>,
6216 at_overlay: Option<TextBounds>,
6217 passes: Vec<&'static str>,
6218 }
6219
6220 impl IdShot {
6221 fn id(&self, (x, y): (u32, u32)) -> u32 {
6222 self.ids[(y * PICK_SIZE + x) as usize]
6223 }
6224 }
6225
6226 fn translated(x: f32, y: f32, z: f32) -> Matrix {
6227 let mut model = identity();
6228 model[3] = [x, y, z, 1.0];
6229 model
6230 }
6231
6232 fn upload_shape(renderer: &mut Renderer, mesh: &Mesh) -> MeshHandle {
6233 renderer
6234 .upload_mesh(MeshData {
6235 positions: &mesh.positions,
6236 normals: &mesh.normals,
6237 tangents: &mesh.tangents,
6238 uvs: &mesh.uvs,
6239 uvs1: None,
6240 alpha: None,
6241 indices: &mesh.indices,
6242 })
6243 .unwrap()
6244 }
6245
6246 fn solid_atlas() -> pfx_text::Atlas {
6247 let level = |size: u32| pfx_text::MipLevel {
6248 width: size,
6249 height: size,
6250 bytes: vec![255; (size * size) as usize],
6251 };
6252 pfx_text::Atlas {
6253 channels: 1,
6254 levels: vec![level(8), level(4), level(2), level(1)],
6255 }
6256 }
6257
6258 fn solid_quad(center: [f32; 2], half: f32, color: [f32; 4]) -> RichQuad {
6259 RichQuad {
6260 rect: [center[0] - half, center[1] - half, 2.0 * half, 2.0 * half],
6261 uv: [0.0, 0.0, 1.0, 1.0],
6262 origin: center,
6263 alpha: 1.0,
6264 clip: [-1000.0, -1000.0, 1000.0, 1000.0],
6265 turn: [0.0; 3],
6266 color,
6267 }
6268 }
6269
6270 fn id_shot(
6271 glass_id: u32,
6272 text_id: u32,
6273 overlay_id: u32,
6274 lit: bool,
6275 pick: (u32, u32),
6276 ) -> IdShot {
6277 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6278 let mut renderer = Renderer::new(gpu, PICK_SIZE, PICK_SIZE).unwrap();
6279 renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6280 renderer.shadows = Shadows::new(
6281 &renderer.gpu().device,
6282 Quality {
6283 resolution: 256,
6284 ..Default::default()
6285 },
6286 );
6287 let floor = Shape::RoundBox {
6288 half: [3.0, 3.0, 0.1],
6289 radius: 0.02,
6290 }
6291 .mesh(0.5);
6292 let panel = Shape::RoundBox {
6293 half: [0.6, 0.6, 0.05],
6294 radius: 0.02,
6295 }
6296 .mesh(0.2);
6297 let slab = Shape::RoundBox {
6298 half: [0.5, 0.5, 0.2],
6299 radius: 0.05,
6300 }
6301 .mesh(0.2);
6302 let floor = upload_shape(&mut renderer, &floor);
6303 let panel = upload_shape(&mut renderer, &panel);
6304 let slab = renderer.upload_glass(&slab).unwrap();
6305 let instances = [
6306 Instance::new(floor, translated(0.0, 0.0, -1.0), 0, FLOOR_ID),
6307 Instance::new(panel, translated(0.8, 0.0, 0.0), 0, PANEL_ID),
6308 ];
6309 let materials = [Material {
6310 base: [0.6; 3],
6311 roughness: 0.6,
6312 ..Default::default()
6313 }];
6314 let glass = [glass::Surface {
6315 mesh: slab,
6316 model: translated(-0.8, 0.0, 0.0),
6317 material: pfx_materials::fixtures::glass(),
6318 liquid: false,
6319 fluid_height: 0.0,
6320 ripple_height: 0.0,
6321 caustic_strength: 0.0,
6322 tinted: None,
6323 id: glass_id,
6324 casts_shadow: false,
6325 shadow_only: false,
6326 clip: [[0.0; 4]; 2],
6327 }];
6328 let atlas = GpuAtlas::new(
6329 &renderer.gpu().device,
6330 &renderer.gpu().queue,
6331 &solid_atlas(),
6332 )
6333 .unwrap();
6334 let ink = [solid_quad([0.0, 0.0], 30.0, [0.1, 0.1, 0.1, 1.0])];
6335 let badge = [solid_quad(
6336 [OVERLAY_PIXEL.0 as f32, OVERLAY_PIXEL.1 as f32],
6337 8.0,
6338 [1.0, 0.8, 0.2, 1.0],
6339 )];
6340 let mut view = identity();
6341 view[3][2] = -4.0;
6342 let projection = perspective(1.0);
6343 let camera = Camera {
6344 view,
6345 projection,
6346 previous_view_projection: frame::multiply(projection, view),
6347 position: [0.0, 0.0, 4.0],
6348 };
6349 let text_model = [
6350 [0.01, 0.0, 0.0, 0.0],
6351 [0.0, -0.01, 0.0, 0.0],
6352 [0.0, 0.0, 1.0, 0.0],
6353 [0.8, 0.0, 0.08, 1.0],
6354 ];
6355 let model_view_projection = frame::multiply(frame::multiply(projection, view), text_model);
6356 let text_space = if lit {
6357 TextSpace::LitSurface {
6358 model_view_projection,
6359 model: text_model,
6360 }
6361 } else {
6362 TextSpace::Surface {
6363 model_view_projection,
6364 }
6365 };
6366 let text = Text {
6367 surface_quads: vec![TextQuadItem {
6368 atlas: &atlas,
6369 quads: &ink,
6370 space: text_space,
6371 icons: false,
6372 id: text_id,
6373 }],
6374 overlay_quads: vec![TextQuadItem {
6375 atlas: &atlas,
6376 quads: &badge,
6377 space: TextSpace::Overlay {
6378 width: PICK_SIZE,
6379 height: PICK_SIZE,
6380 },
6381 icons: false,
6382 id: overlay_id,
6383 }],
6384 ..Default::default()
6385 };
6386 let effects = Effects {
6387 glass: &glass,
6388 ..Default::default()
6389 };
6390 let target = output(renderer.gpu(), PICK_SIZE, PICK_SIZE);
6391 renderer.pick(pick.0, pick.1).unwrap();
6392 for number in 0..4 {
6393 let scene = Scene {
6394 camera,
6395 time: number as f32 / 60.0,
6396 seed: 5,
6397 sun: Sun {
6398 direction: [0.3, 0.5, 0.8],
6399 colour: [1.0; 3],
6400 intensity: 2.0,
6401 },
6402 instances: &instances,
6403 materials: &materials,
6404 deformers: &[],
6405 wind: SceneWind::default(),
6406 };
6407 renderer
6408 .render(&scene, &text, &effects, Finish::Standard, &target.view)
6409 .unwrap();
6410 }
6411 let colour = renderer.gpu().readback_rgba16(&target).unwrap();
6412 let ids = readback_texture(
6413 renderer.gpu(),
6414 &renderer.frame().targets.ids,
6415 PICK_SIZE,
6416 PICK_SIZE,
6417 4,
6418 )
6419 .unwrap()
6420 .chunks_exact(4)
6421 .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
6422 .collect();
6423 let mut picked = None;
6424 for _ in 0..100 {
6425 picked = renderer.picked();
6426 if picked.is_some() {
6427 break;
6428 }
6429 let _ = renderer
6430 .gpu()
6431 .device
6432 .poll(wgpu::PollType::wait_indefinitely());
6433 }
6434 IdShot {
6435 colour,
6436 ids,
6437 picked: picked.expect("the pick never came back"),
6438 bounds: renderer.text_bounds().to_vec(),
6439 at_overlay: renderer
6440 .text_at(OVERLAY_PIXEL.0 as f32 + 0.5, OVERLAY_PIXEL.1 as f32 + 0.5)
6441 .copied(),
6442 passes: renderer.last_pass_order().to_vec(),
6443 }
6444 }
6445
6446 #[derive(Clone, Copy, PartialEq)]
6447 enum Caster {
6448 Nothing,
6449 Copy { moving: bool },
6450 Glass,
6451 }
6452
6453 const CAST_SIZE: u32 = 256;
6454
6455 struct CastShot {
6456 colour: Vec<u16>,
6457 passes: Vec<&'static str>,
6458 transmission: f64,
6459 cascades: f64,
6460 opaque: f64,
6461 }
6462
6463 fn cast_glass() -> Material {
6464 Material {
6465 base: [0.5, 0.62, 0.95],
6466 ..pfx_materials::fixtures::glass()
6467 }
6468 }
6469
6470 fn cast_camera() -> Camera {
6471 let mut view = identity();
6472 view[3][2] = -4.0;
6473 let projection = perspective(1.0);
6474 Camera {
6475 view,
6476 projection,
6477 previous_view_projection: frame::multiply(projection, view),
6478 position: [0.0, 0.0, 4.0],
6479 }
6480 }
6481
6482 fn cast_pixel(world: [f32; 3]) -> usize {
6483 let camera = cast_camera();
6484 let clip = frame::transform(
6485 frame::multiply(camera.projection, camera.view),
6486 [world[0], world[1], world[2], 1.0],
6487 );
6488 let x = ((clip[0] / clip[3] * 0.5 + 0.5) * CAST_SIZE as f32) as usize;
6489 let y = ((0.5 - clip[1] / clip[3] * 0.5) * CAST_SIZE as f32) as usize;
6490 y * CAST_SIZE as usize + x
6491 }
6492
6493 fn cast_shot(caster: Caster, frames: usize) -> CastShot {
6494 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6495 let mut renderer = Renderer::new(gpu, CAST_SIZE, CAST_SIZE).unwrap();
6496 renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6497 let floor = Shape::RoundBox {
6498 half: [3.0, 3.0, 0.1],
6499 radius: 0.02,
6500 }
6501 .mesh(0.5);
6502 let slab = Shape::RoundBox {
6503 half: [0.5, 0.5, 0.2],
6504 radius: 0.05,
6505 }
6506 .mesh(0.2);
6507 let floor = upload_shape(&mut renderer, &floor);
6508 let glass_mesh = renderer.upload_glass(&slab).unwrap();
6509 let mut instances = vec![Instance::new(floor, translated(0.0, 0.0, -1.0), 0, 1)];
6510 let glass_model = translated(-0.8, 0.0, 0.0);
6511 if let Caster::Copy { moving } = caster {
6512 let copy = upload_shape(&mut renderer, &slab);
6513 let mut instance = Instance::new(copy, glass_model, 1, 2);
6514 instance.shadow_only = true;
6515 if moving {
6516 instance.previous_model[3][1] = 1.0e-9;
6517 }
6518 instances.push(instance);
6519 renderer
6520 .set_transmission(&[None, Some(glass::cast_tint(&cast_glass()))])
6521 .unwrap();
6522 }
6523 let materials = [
6524 Material {
6525 base: [0.6; 3],
6526 roughness: 0.6,
6527 ..Default::default()
6528 },
6529 Material::default(),
6530 ];
6531 let glass = [glass::Surface {
6532 mesh: glass_mesh,
6533 model: glass_model,
6534 material: cast_glass(),
6535 liquid: false,
6536 fluid_height: 0.0,
6537 ripple_height: 0.0,
6538 caustic_strength: 0.0,
6539 tinted: None,
6540 id: 0,
6541 casts_shadow: caster == Caster::Glass,
6542 shadow_only: false,
6543 clip: [[0.0; 4]; 2],
6544 }];
6545 let effects = Effects {
6546 glass: &glass,
6547 ..Default::default()
6548 };
6549 let target = output(renderer.gpu(), CAST_SIZE, CAST_SIZE);
6550 let mut sums = [0.0; 3];
6551 let mut timed = 0;
6552 let mut turns = pfx_gpu::pace::Turns::default();
6553 for number in 0..frames {
6554 let scene = Scene {
6555 camera: cast_camera(),
6556 time: number as f32 / 60.0,
6557 seed: 5,
6558 sun: Sun {
6559 direction: [0.3, 0.5, 0.8],
6560 colour: [1.0; 3],
6561 intensity: 2.0,
6562 },
6563 instances: &instances,
6564 materials: &materials,
6565 deformers: &[],
6566 wind: SceneWind::default(),
6567 };
6568 let timings = renderer
6569 .render(
6570 &scene,
6571 &Text::default(),
6572 &effects,
6573 Finish::Standard,
6574 &target.view,
6575 )
6576 .unwrap();
6577 turns.add(pfx_gpu::pace::gpu_ms(&timings).unwrap_or(f64::NAN));
6578 if number < 4 || timings.is_empty() {
6579 continue;
6580 }
6581 timed += 1;
6582 for timing in &timings {
6583 let slot = if timing.label == "shadow transmission" {
6584 0
6585 } else if timing.label.starts_with("shadow cascade") {
6586 1
6587 } else if timing.label == "opaque PBR" {
6588 2
6589 } else {
6590 continue;
6591 };
6592 sums[slot] += timing.milliseconds;
6593 }
6594 }
6595 let mean = |sum: f64| sum / f64::from(timed.max(1));
6596 CastShot {
6597 colour: renderer.gpu().readback_rgba16(&target).unwrap(),
6598 passes: renderer.last_pass_order().to_vec(),
6599 transmission: mean(sums[0]),
6600 cascades: mean(sums[1]),
6601 opaque: mean(sums[2]),
6602 }
6603 }
6604
6605 fn rgb(colour: &[u16], at: usize) -> [f32; 3] {
6606 [0, 1, 2].map(|k| half::f16::from_bits(colour[at * 4 + k]).to_f32())
6607 }
6608
6609 #[test]
6610 fn a_glass_cast_tint_is_transmission_times_base_or_beer_lambert_over_its_thickness() {
6611 let clear = Material {
6612 base: [0.5, 0.8, 1.0],
6613 transmission: 0.9,
6614 ..Material::default()
6615 };
6616 let tint = glass::cast_tint(&clear);
6617 for (k, expected) in [0.45, 0.72, 0.9].into_iter().enumerate() {
6618 assert!((tint[k] - expected).abs() < 1e-6, "{tint:?}");
6619 }
6620 let thick = Material {
6621 thickness: 0.3,
6622 absorption: 4.0,
6623 ..clear
6624 };
6625 let beer = pfx_materials::beer(0.3, [0.5, 0.8, 1.0], 4.0);
6626 let tint = glass::cast_tint(&thick);
6627 for k in 0..3 {
6628 assert!((tint[k] - beer[k] * 0.9).abs() < 1e-6, "{tint:?}");
6629 }
6630 assert_eq!(tint[2], 0.9);
6631 let unthick = Material {
6632 absorption: 4.0,
6633 ..clear
6634 };
6635 assert_eq!(glass::cast_tint(&unthick), glass::cast_tint(&clear));
6636 let bright = Material {
6637 base: [2.0, 0.5, -1.0],
6638 transmission: 1.0,
6639 ..Material::default()
6640 };
6641 assert_eq!(glass::cast_tint(&bright), [1.0, 0.5, 0.0]);
6642 }
6643
6644 #[test]
6645 #[ignore = "needs a GPU; run under pgpu"]
6646 fn glass_casts_the_shadow_its_shadow_only_copy_cast() {
6647 let nothing = cast_shot(Caster::Nothing, 6);
6648 let copy = cast_shot(Caster::Copy { moving: false }, 6);
6649 let glass = cast_shot(Caster::Glass, 6);
6650 assert!(!nothing.passes.contains(&"shadow transmission"));
6651 assert!(glass.passes.contains(&"shadow transmission"));
6652 let shade = cast_pixel([-1.1, -0.9, -0.9]);
6653 let open = cast_pixel([1.5, -0.9, -0.9]);
6654 let ratio = |shot: &CastShot| {
6655 let (lit, shaded) = (rgb(&shot.colour, open), rgb(&shot.colour, shade));
6656 [0, 1, 2].map(|k| shaded[k] / lit[k])
6657 };
6658 println!(
6659 "under the slab / open floor: nothing {:?}, copy {:?}, glass {:?}",
6660 ratio(¬hing),
6661 ratio(©),
6662 ratio(&glass)
6663 );
6664 let glassed = ratio(&glass);
6665 assert!(glassed[2] > glassed[0] + 0.1, "the shadow is not tinted");
6666 assert!(
6667 glassed[0] < ratio(¬hing)[0] - 0.1,
6668 "the glass casts no shadow"
6669 );
6670 let mut worst = 0.0f32;
6671 let mut differing = 0;
6672 for (a, b) in copy
6673 .colour
6674 .chunks_exact(4)
6675 .zip(glass.colour.chunks_exact(4))
6676 {
6677 let gap = (0..3)
6678 .map(|k| {
6679 (half::f16::from_bits(a[k]).to_f32() - half::f16::from_bits(b[k]).to_f32())
6680 .abs()
6681 })
6682 .fold(0.0, f32::max);
6683 worst = worst.max(gap);
6684 differing += usize::from(gap > 0.0);
6685 }
6686 println!("copy against glass: {differing} pixels differ, worst {worst:.5}");
6687 assert!(worst <= 1.0 / 255.0, "worst {worst}");
6688 }
6689
6690 #[test]
6691 #[ignore = "needs a GPU; run under pgpu"]
6692 fn glass_casting_costs_about_what_its_copy_costs() {
6693 for (name, caster) in [
6694 ("nothing", Caster::Nothing),
6695 ("still copy", Caster::Copy { moving: false }),
6696 ("moving copy", Caster::Copy { moving: true }),
6697 ("glass", Caster::Glass),
6698 ] {
6699 let shot = cast_shot(caster, 64);
6700 println!(
6701 "{name}: transmission {:.4} ms, cascades {:.4} ms, opaque {:.4} ms",
6702 shot.transmission, shot.cascades, shot.opaque
6703 );
6704 }
6705 }
6706
6707 #[derive(Clone, Copy, Debug, PartialEq)]
6708 enum Card {
6709 Plain,
6710 Cut,
6711 Grid,
6712 }
6713
6714 const CARD_IMAGE: u32 = 256;
6715 const CARD_CELLS: u32 = 128;
6716
6717 fn card_alpha(u: f32, v: f32) -> f32 {
6718 let r = ((u - 0.5).powi(2) + (v - 0.5).powi(2)).sqrt();
6719 if (0.25..=0.45).contains(&r) { 1.0 } else { 0.0 }
6720 }
6721
6722 fn card_mesh(renderer: &mut Renderer, card: Card, half: f32) -> MeshHandle {
6723 let cells = if card == Card::Grid { CARD_CELLS } else { 1 };
6724 let side = cells + 1;
6725 let mut positions = Vec::new();
6726 let mut uvs = Vec::new();
6727 let mut alpha = Vec::new();
6728 for j in 0..side {
6729 for i in 0..side {
6730 let uv = [i as f32 / cells as f32, j as f32 / cells as f32];
6731 positions.push([(uv[0] * 2.0 - 1.0) * half, 0.0, (uv[1] * 2.0 - 1.0) * half]);
6732 uvs.push(uv);
6733 alpha.push(card_alpha(uv[0], uv[1]));
6734 }
6735 }
6736 let mut indices = Vec::new();
6737 for j in 0..cells {
6738 for i in 0..cells {
6739 let a = j * side + i;
6740 indices.extend([a, a + side, a + 1, a + 1, a + side, a + side + 1]);
6741 }
6742 }
6743 let count = positions.len();
6744 renderer
6745 .upload_mesh(MeshData {
6746 positions: &positions,
6747 normals: &vec![[0.0, 1.0, 0.0]; count],
6748 tangents: &vec![[1.0, 0.0, 0.0, 1.0]; count],
6749 uvs: &uvs,
6750 uvs1: None,
6751 alpha: (card == Card::Grid).then_some(alpha.as_slice()),
6752 indices: &indices,
6753 })
6754 .unwrap()
6755 }
6756
6757 fn card_costs(card: Card) -> [f64; 3] {
6758 let (width, height) = (3840, 2160);
6759 let gpu = pollster::block_on(Gpu::headless()).unwrap();
6760 let mut renderer = Renderer::new(gpu, width, height).unwrap();
6761 renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6762 let floor = card_mesh(&mut renderer, Card::Plain, 4.0);
6763 let mesh = card_mesh(&mut renderer, card, 0.3);
6764 let texels: Vec<u8> = (0..CARD_IMAGE * CARD_IMAGE)
6765 .flat_map(|index| {
6766 let u = ((index % CARD_IMAGE) as f32 + 0.5) / CARD_IMAGE as f32;
6767 let v = ((index / CARD_IMAGE) as f32 + 0.5) / CARD_IMAGE as f32;
6768 [220, 60, 40, (card_alpha(u, v) * 255.0) as u8]
6769 })
6770 .collect();
6771 renderer
6772 .frame()
6773 .set_content(0, CARD_IMAGE, CARD_IMAGE, crate::maps::ContentFormat::Srgb8)
6774 .unwrap();
6775 renderer
6776 .frame()
6777 .write_content(0, [0, 0, CARD_IMAGE, CARD_IMAGE], &texels)
6778 .unwrap();
6779 let mut instances = vec![Instance::new(floor, identity(), 0, 1)];
6780 for row in 0..4 {
6781 for column in 0..6 {
6782 let model = translated(
6783 (column as f32 - 2.5) * 0.8,
6784 0.8 + 0.05 * row as f32,
6785 (row as f32 - 1.5) * 0.8,
6786 );
6787 let mut instance = Instance::new(mesh, model, 1, instances.len() as u32 + 1);
6788 instance.previous_model[3][1] += 1.0e-6;
6789 instances.push(instance);
6790 }
6791 }
6792 let mut surfaces = vec![
6793 frame::InstanceSurface {
6794 cutout: card == Card::Cut,
6795 ..Default::default()
6796 };
6797 instances.len()
6798 ];
6799 surfaces[0].cutout = false;
6800 renderer.frame().set_surfaces(&surfaces).unwrap();
6801 let materials = [
6802 Material {
6803 base: [0.6; 3],
6804 roughness: 0.7,
6805 ..Default::default()
6806 },
6807 Material {
6808 base: [0.9; 3],
6809 roughness: 0.5,
6810 content_layer: pfx_materials::ContentLayer {
6811 slot: 0,
6812 ..Default::default()
6813 },
6814 ..Default::default()
6815 },
6816 ];
6817 let view = [
6818 [1.0, 0.0, 0.0, 0.0],
6819 [0.0, 0.0, 1.0, 0.0],
6820 [0.0, -1.0, 0.0, 0.0],
6821 [0.0, 0.0, -4.0, 1.0],
6822 ];
6823 let projection = perspective(width as f32 / height as f32);
6824 let camera = Camera {
6825 view,
6826 projection,
6827 previous_view_projection: frame::multiply(projection, view),
6828 position: [0.0, 4.0, 0.0],
6829 };
6830 let target = output(renderer.gpu(), width, height);
6831 let mut sums = [0.0; 3];
6832 let mut timed = 0;
6833 let mut turns = pfx_gpu::pace::Turns::default();
6834 for number in 0..40 {
6835 let scene = Scene {
6836 camera,
6837 time: number as f32 / 60.0,
6838 seed: 5,
6839 sun: Sun {
6840 direction: [0.8, 0.6, 0.0],
6841 colour: [1.0; 3],
6842 intensity: 3.0,
6843 },
6844 instances: &instances,
6845 materials: &materials,
6846 deformers: &[],
6847 wind: SceneWind::default(),
6848 };
6849 let timings = renderer
6850 .render(
6851 &scene,
6852 &Text::default(),
6853 &Effects::default(),
6854 Finish::Standard,
6855 &target.view,
6856 )
6857 .unwrap();
6858 turns.add(pfx_gpu::pace::gpu_ms(&timings).unwrap_or(f64::NAN));
6859 if number < 8 || timings.is_empty() {
6860 continue;
6861 }
6862 timed += 1;
6863 for timing in &timings {
6864 let slot = if timing.label == "depth and velocity" {
6865 0
6866 } else if timing.label == "opaque PBR" {
6867 1
6868 } else if timing.label.starts_with("shadow cascade") {
6869 2
6870 } else {
6871 continue;
6872 };
6873 sums[slot] += timing.milliseconds;
6874 }
6875 }
6876 sums.map(|sum| sum / f64::from(timed.max(1)))
6877 }
6878
6879 #[test]
6880 #[ignore = "needs a GPU; run under pgpu"]
6881 fn card_scene_costs_with_content_cutouts_and_vertex_alpha_grids() {
6882 for card in [Card::Plain, Card::Cut, Card::Grid] {
6883 let [depth, opaque, cascades] = card_costs(card);
6884 println!(
6885 "{card:?} cards at 3840x2160: depth and velocity {depth:.4} ms, opaque {opaque:.4} ms, cascades {cascades:.4} ms"
6886 );
6887 }
6888 }
6889
6890 #[test]
6891 #[ignore = "needs a GPU; run under pgpu"]
6892 fn glass_with_an_id_picks_over_the_floor_and_glass_without_one_lets_it_through() {
6893 let picked = id_shot(9, 0, 0, false, GLASS_PIXEL);
6894 assert_eq!(picked.picked.id, 9);
6895 assert_eq!(picked.id(GLASS_PIXEL), 9);
6896 assert_eq!(picked.id(FLOOR_PIXEL), FLOOR_ID);
6897 assert_eq!(picked.id(PANEL_PIXEL), PANEL_ID);
6898 assert!(picked.passes.contains(&"glass ids"));
6899 let through = id_shot(0, 0, 0, false, GLASS_PIXEL);
6900 assert_eq!(through.picked.id, FLOOR_ID);
6901 assert_eq!(through.id(GLASS_PIXEL), FLOOR_ID);
6902 assert!(!through.passes.contains(&"glass ids"));
6903 let slab = picked.ids.iter().filter(|id| **id == 9).count();
6904 let changed = picked
6905 .ids
6906 .iter()
6907 .zip(&through.ids)
6908 .filter(|(a, b)| a != b)
6909 .count();
6910 assert!(slab > 400, "the slab covers only {slab} pixels");
6911 assert_eq!(changed, slab, "ids changed outside the slab");
6912 }
6913
6914 #[test]
6915 #[ignore = "needs a GPU; run under pgpu"]
6916 fn surface_text_picks_as_its_own_id_on_its_panel() {
6917 for lit in [false, true] {
6918 let shot = id_shot(0, 21, 0, lit, TEXT_PIXEL);
6919 assert_eq!(shot.picked.id, 21, "lit {lit}");
6920 assert_eq!(shot.id(TEXT_PIXEL), 21, "lit {lit}");
6921 assert_eq!(shot.id(PANEL_PIXEL), PANEL_ID, "lit {lit}");
6922 assert!(shot.passes.contains(&"text ids"));
6923 let plain = id_shot(0, 0, 0, lit, TEXT_PIXEL);
6924 assert_eq!(plain.picked.id, PANEL_ID, "lit {lit}");
6925 assert!(!plain.passes.contains(&"text ids"));
6926 }
6927 }
6928
6929 #[test]
6930 #[ignore = "needs a GPU; run under pgpu"]
6931 fn overlay_text_picks_over_a_mesh_and_its_bounds_come_back_on_the_cpu() {
6932 let shot = id_shot(0, 21, 33, false, OVERLAY_PIXEL);
6933 assert_eq!(shot.picked.id, 33);
6934 assert_eq!(shot.id(OVERLAY_PIXEL), 33);
6935 assert_eq!(shot.id(FLOOR_PIXEL), FLOOR_ID);
6936 let plain = id_shot(0, 21, 0, false, OVERLAY_PIXEL);
6937 assert_eq!(plain.picked.id, FLOOR_ID);
6938 let overlay = shot.at_overlay.expect("no text bounds under the overlay");
6939 assert_eq!(
6940 (overlay.list, overlay.index, overlay.id),
6941 (TextList::OverlayQuads, 0, 33)
6942 );
6943 let close = |a: [f32; 4], b: [f32; 4]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 0.01);
6944 assert!(
6945 close(overlay.rect, [56.0, 104.0, 72.0, 120.0]),
6946 "{:?}",
6947 overlay.rect
6948 );
6949 let surface = shot
6950 .bounds
6951 .iter()
6952 .find(|bounds| bounds.list == TextList::SurfaceQuads)
6953 .expect("no surface text bounds");
6954 assert_eq!(surface.id, 21);
6955 let [left, top, right, bottom] = surface.rect;
6956 let (x, y) = (TEXT_PIXEL.0 as f32, TEXT_PIXEL.1 as f32);
6957 assert!(
6958 left < x && x < right && top < y && y < bottom,
6959 "{:?}",
6960 surface.rect
6961 );
6962 let covered = shot.ids.iter().enumerate().filter(|(_, id)| **id == 21);
6963 for (index, _) in covered {
6964 let px = (index as u32 % PICK_SIZE) as f32 + 0.5;
6965 let py = (index as u32 / PICK_SIZE) as f32 + 0.5;
6966 assert!(
6967 left <= px && px <= right && top <= py && py <= bottom,
6968 "text id at ({px}, {py}) outside its bounds {:?}",
6969 surface.rect
6970 );
6971 }
6972 }
6973
6974 #[test]
6975 #[ignore = "needs a GPU; run under pgpu"]
6976 fn id_writes_leave_the_frames_colour_bytes_unchanged() {
6977 for lit in [false, true] {
6978 let before = id_shot(0, 0, 0, lit, GLASS_PIXEL);
6979 let after = id_shot(9, 21, 33, lit, GLASS_PIXEL);
6980 assert_eq!(after.id(GLASS_PIXEL), 9);
6981 assert_eq!(after.id(TEXT_PIXEL), 21);
6982 assert_eq!(after.id(OVERLAY_PIXEL), 33);
6983 let differ = before
6984 .colour
6985 .iter()
6986 .zip(&after.colour)
6987 .filter(|(a, b)| a != b)
6988 .count();
6989 assert_eq!(differ, 0, "lit {lit}: {differ} colour channels changed");
6990 }
6991 }
6992
6993 fn glass_slab(half: f32, step: f32) -> Mesh {
6994 Shape::RoundBox {
6995 half: [half, half, 0.2],
6996 radius: 0.05,
6997 }
6998 .mesh(step)
6999 }
7000
7001 fn render_glass(
7002 renderer: &mut Renderer,
7003 handle: glass::MeshHandle,
7004 target: &OffscreenTarget,
7005 ) -> Result<(), String> {
7006 let mut view = identity();
7007 view[3][2] = -4.0;
7008 let projection = perspective(1.0);
7009 let camera = Camera {
7010 view,
7011 projection,
7012 previous_view_projection: frame::multiply(projection, view),
7013 position: [0.0, 0.0, 4.0],
7014 };
7015 let materials = [Material::default()];
7016 let glass = [glass::Surface {
7017 mesh: handle,
7018 model: identity(),
7019 material: pfx_materials::fixtures::glass(),
7020 liquid: false,
7021 fluid_height: 0.0,
7022 ripple_height: 0.0,
7023 caustic_strength: 0.0,
7024 tinted: None,
7025 id: 0,
7026 casts_shadow: false,
7027 shadow_only: false,
7028 clip: [[0.0; 4]; 2],
7029 }];
7030 let scene = Scene {
7031 camera,
7032 time: 0.0,
7033 seed: 5,
7034 sun: Sun {
7035 direction: [0.3, 0.5, 0.8],
7036 colour: [1.0; 3],
7037 intensity: 2.0,
7038 },
7039 instances: &[],
7040 materials: &materials,
7041 deformers: &[],
7042 wind: SceneWind::default(),
7043 };
7044 let effects = Effects {
7045 glass: &glass,
7046 ..Default::default()
7047 };
7048 renderer
7049 .render(
7050 &scene,
7051 &Text::default(),
7052 &effects,
7053 Finish::Standard,
7054 &target.view,
7055 )
7056 .map(|_| ())
7057 }
7058
7059 #[test]
7060 #[ignore = "needs a GPU; run under pgpu"]
7061 fn glass_meshes_release_replace_and_keep_their_handles_through_a_resize() {
7062 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7063 let mut renderer = Renderer::new(gpu, PICK_SIZE, PICK_SIZE).unwrap();
7064 let target = output(renderer.gpu(), PICK_SIZE, PICK_SIZE);
7065 let small = glass_slab(0.3, 0.3);
7066 let large = glass_slab(0.5, 0.1);
7067 let keep = renderer.upload_glass(&small).unwrap();
7068 let flat = renderer.mesh_stats();
7069 assert_eq!(
7070 (flat.glass_live, flat.glass_slots, flat.glass_buffers),
7071 (1, 1, 2)
7072 );
7073 render_glass(&mut renderer, keep, &target).unwrap();
7074 for cycle in 0..1000 {
7075 let (first, second) = if cycle % 2 == 0 {
7076 (&small, &large)
7077 } else {
7078 (&large, &small)
7079 };
7080 let handle = renderer.upload_glass(first).unwrap();
7081 assert_eq!(renderer.mesh_stats().glass_live, 2);
7082 renderer.replace_glass(handle, second).unwrap();
7083 renderer.replace_glass(handle, first).unwrap();
7084 renderer.release_glass(handle).unwrap();
7085 assert!(renderer.release_glass(handle).is_err());
7086 assert!(renderer.replace_glass(handle, first).is_err());
7087 assert!(renderer.glass_mesh_buffers(handle).is_none());
7088 let stats = renderer.mesh_stats();
7089 assert_eq!(
7090 (stats.glass_live, stats.glass_slots, stats.glass_buffers),
7091 (1, 2, 2),
7092 "cycle {cycle}"
7093 );
7094 }
7095 let before = renderer.glass_mesh_buffers(keep).unwrap().0.clone();
7096 renderer.replace_glass(keep, &small).unwrap();
7097 assert!(*renderer.glass_mesh_buffers(keep).unwrap().0 == before);
7098 renderer.replace_glass(keep, &large).unwrap();
7099 assert_eq!(renderer.mesh_stats().glass_buffers, 2);
7100 renderer.replace_glass(keep, &small).unwrap();
7101 assert_eq!(
7102 renderer.glass_mesh_buffers(keep).unwrap().2,
7103 small.indices.len() as u32
7104 );
7105 renderer.resize(96, 96).unwrap();
7106 let resized = renderer.mesh_stats();
7107 assert_eq!(
7108 (
7109 resized.glass_live,
7110 resized.glass_slots,
7111 resized.glass_buffers
7112 ),
7113 (1, 2, 2)
7114 );
7115 let target = output(renderer.gpu(), 96, 96);
7116 render_glass(&mut renderer, keep, &target).unwrap();
7117 renderer.release_glass(keep).unwrap();
7118 let error = render_glass(&mut renderer, keep, &target).unwrap_err();
7119 assert!(error.contains("released"), "{error}");
7120 let end = renderer.mesh_stats();
7121 assert_eq!(
7122 (end.glass_live, end.glass_buffers),
7123 (0, 0),
7124 "the last glass mesh was released"
7125 );
7126 }
7127
7128 fn flat_board() -> (crate::flat::ShadowCurve, Vec<crate::flat::Draw>) {
7129 use crate::flat::{CurvePoint, Draw, ShadowCurve, Srgba, Stroke};
7130 let curve = ShadowCurve::new(
7131 Srgba::hex(0x2b3442),
7132 vec![
7133 CurvePoint {
7134 elevation: 1.0,
7135 offset: 4.0,
7136 sigma: 5.0,
7137 alpha: 0.22,
7138 },
7139 CurvePoint {
7140 elevation: 2.0,
7141 offset: 12.0,
7142 sigma: 16.0,
7143 alpha: 0.26,
7144 },
7145 ],
7146 )
7147 .unwrap();
7148 let draws = vec![
7149 Draw::rect([48.0, 40.0], [64.0, 40.0], 8.0)
7150 .fill(Srgba::WHITE)
7151 .elevation(1.0)
7152 .id(7),
7153 Draw::circle([30.0, 40.0], 9.0)
7154 .fill(Srgba::hex(0xe2614c))
7155 .elevation(1.0)
7156 .shadow(crate::flat::Shadow::None)
7157 .id(8),
7158 Draw::rect([70.0, 70.0], [40.0, 30.0], 10.0)
7159 .vertical(Srgba::hex(0xffd36b), Srgba::hex(0xf0a92e))
7160 .stroke(Stroke::solid(Srgba::hex(0xc4840c), 2.0))
7161 .elevation(2.0)
7162 .id(9),
7163 ];
7164 (curve, draws)
7165 }
7166
7167 fn flat_scene<'a>(
7168 curve: &'a crate::flat::ShadowCurve,
7169 draws: &'a [crate::flat::Draw],
7170 ) -> crate::flat::FlatScene<'a> {
7171 crate::flat::FlatScene {
7172 layout: [96.0, 96.0],
7173 clear: Some(crate::flat::Srgba::hex(0xeceff3)),
7174 curve,
7175 light: crate::flat::Light::default(),
7176 draws,
7177 groups: &[],
7178 text: &[],
7179 icons: None,
7180 sprites: None,
7181 environment: None,
7182 post: false,
7183 frame: 0,
7184 seed: 0,
7185 }
7186 }
7187
7188 #[test]
7189 #[ignore = "needs a GPU; run under pgpu"]
7190 fn existing_scenes_render_the_same_bytes_beside_a_flat_hud() {
7191 let (mut plain, plain_target, instances, materials) = sphere_still(96);
7192 let (mut beside, beside_target, _, _) = sphere_still(96);
7193 let (curve, _) = flat_board();
7194 let empty = flat_scene(&curve, &[]);
7195 for number in 0..4 {
7196 let scene = still_scene(&instances, &materials, number);
7197 plain
7198 .render(
7199 &scene,
7200 &Text::default(),
7201 &Effects::default(),
7202 Finish::Standard,
7203 &plain_target.view,
7204 )
7205 .unwrap();
7206 beside
7207 .render_with_flat(
7208 &scene,
7209 &Text::default(),
7210 &Effects::default(),
7211 Finish::Standard,
7212 &empty,
7213 &beside_target.view,
7214 )
7215 .unwrap();
7216 }
7217 assert!(!plain.last_pass_order().contains(&"flat"));
7218 assert!(beside.last_pass_order().contains(&"flat"));
7219 let expected = plain.gpu().readback_rgba16(&plain_target).unwrap();
7220 let actual = beside.gpu().readback_rgba16(&beside_target).unwrap();
7221 assert!(expected == actual, "an empty flat HUD changed the frame");
7222 let (curve, draws) = flat_board();
7223 let hud = flat_scene(&curve, &draws);
7224 let scene = still_scene(&instances, &materials, 4);
7225 beside
7226 .render_with_flat(
7227 &scene,
7228 &Text::default(),
7229 &Effects::default(),
7230 Finish::Standard,
7231 &hud,
7232 &beside_target.view,
7233 )
7234 .unwrap();
7235 let with_hud = beside.gpu().readback_rgba16(&beside_target).unwrap();
7236 let at = |pixels: &[u16], x: usize, y: usize| -> [f32; 4] {
7237 std::array::from_fn(|c| half::f16::from_bits(pixels[(y * 96 + x) * 4 + c]).to_f32())
7238 };
7239 assert!(at(&with_hud, 48, 30)[..3].iter().all(|&c| c > 0.995));
7240 let red = at(&with_hud, 30, 40);
7241 assert!(
7242 (red[0] - 0xe2 as f32 / 255.0).abs() < 2e-3
7243 && (red[1] - 0x61 as f32 / 255.0).abs() < 2e-3
7244 );
7245 }
7246
7247 #[test]
7248 #[ignore = "needs a GPU; run under pgpu"]
7249 fn a_flat_scene_renders_alone_in_every_output_format_and_picks() {
7250 let (curve, draws) = flat_board();
7251 let scene = flat_scene(&curve, &draws);
7252 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7253 let mut direct = Renderer::new(gpu, 96, 96).unwrap();
7254 let target = output(direct.gpu(), 96, 96);
7255 direct.render_flat(&scene, &target.view).unwrap();
7256 assert_eq!(direct.last_pass_order(), ["flat", "flat ids"]);
7257 let gamma = direct
7258 .gpu()
7259 .readback_rgba16(&target)
7260 .unwrap()
7261 .chunks_exact(4)
7262 .map(|p| std::array::from_fn::<f32, 4, _>(|c| half::f16::from_bits(p[c]).to_f32()))
7263 .collect::<Vec<_>>();
7264 let twin = crate::flat::cpu::render(&scene, None, [96, 96]).unwrap();
7265 for (a, b) in gamma.iter().zip(&twin) {
7266 for c in 0..4 {
7267 assert!((a[c] - b[c]).abs() < 2.5 / 255.0, "{a:?} vs {b:?}");
7268 }
7269 }
7270 assert!(gamma[30 * 96 + 48][..3].iter().all(|&c| c > 0.995));
7271 direct.pick(30, 40).unwrap();
7272 direct.render_flat(&scene, &target.view).unwrap();
7273 let picked = direct.picked().unwrap();
7274 assert_eq!((picked.id, picked.depth), (8, 0.0));
7275 direct.pick(70, 70).unwrap();
7276 direct.render_flat(&scene, &target.view).unwrap();
7277 assert_eq!(direct.picked().unwrap().id, 9);
7278 direct.pick(2, 2).unwrap();
7279 direct.render_flat(&scene, &target.view).unwrap();
7280 assert_eq!(direct.picked().unwrap().id, 0);
7281
7282 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7283 let mut srgb =
7284 Renderer::new_with_output_format(gpu, 96, 96, wgpu::TextureFormat::Rgba8UnormSrgb)
7285 .unwrap();
7286 let srgb_target = srgb
7287 .gpu()
7288 .offscreen(96, 96, wgpu::TextureFormat::Rgba8UnormSrgb)
7289 .unwrap();
7290 srgb.render_flat(&scene, &srgb_target.view).unwrap();
7291 assert_eq!(srgb.last_pass_order(), ["flat", "flat finish", "flat ids"]);
7292 let bytes = srgb.gpu().readback_rgba8(&srgb_target).unwrap();
7293 for (pixel, expected) in bytes.chunks_exact(4).zip(&gamma) {
7294 for c in 0..3 {
7295 let want = (expected[c].clamp(0.0, 1.0) * 255.0).round();
7296 assert!(
7297 (f32::from(pixel[c]) - want).abs() <= 1.0,
7298 "{pixel:?} vs {expected:?}"
7299 );
7300 }
7301 }
7302
7303 let gpu = pollster::block_on(Gpu::headless()).unwrap();
7304 let mut posted = Renderer::new(gpu, 96, 96).unwrap();
7305 posted.set_finish(pfx_post::Chain {
7306 passes: Vec::new(),
7307 frame: 0,
7308 seed: 0,
7309 });
7310 let posted_target = output(posted.gpu(), 96, 96);
7311 let mut through_post = scene;
7312 through_post.post = true;
7313 posted
7314 .render_flat(&through_post, &posted_target.view)
7315 .unwrap();
7316 assert_eq!(
7317 posted.last_pass_order(),
7318 ["flat", "flat finish", "post", "flat ids"]
7319 );
7320 let finished = posted.gpu().readback_rgba16(&posted_target).unwrap();
7321 for (pixel, expected) in finished.chunks_exact(4).zip(&gamma) {
7322 for c in 0..3 {
7323 let value = half::f16::from_bits(pixel[c]).to_f32();
7324 assert!(
7325 (value - expected[c]).abs() < 1.5 / 255.0,
7326 "{value} vs {expected:?}"
7327 );
7328 }
7329 }
7330 posted.set_tape(Some(pfx_post::Tape::forward(1.0)));
7331 let mut glitched = through_post;
7332 glitched.frame = 7;
7333 posted.render_flat(&glitched, &posted_target.view).unwrap();
7334 let taped = posted.gpu().readback_rgba16(&posted_target).unwrap();
7335 assert!(
7336 taped != finished,
7337 "the tape glitch did not reach the flat frame"
7338 );
7339 }
7340}