Skip to main content

pfx_live/
sky.rs

1use bytemuck::{Pod, Zeroable};
2#[cfg(test)]
3use pfx_core::daylight::Daylight;
4use pfx_core::daylight::{self, Probe};
5pub use pfx_core::sky::AnalyticSky;
6use pfx_core::sky::SKY_MODEL_WGSL;
7use pfx_load::Sky as HdrSky;
8use std::f32::consts::{PI, TAU};
9use std::sync::{Arc, Condvar, LazyLock, Mutex, MutexGuard};
10use std::thread::JoinHandle;
11
12const SH_SAMPLES: usize = 4096;
13
14#[derive(Clone, Debug)]
15pub enum SkySource {
16    Analytic(AnalyticSky),
17    Hdr(HdrSky),
18}
19
20impl SkySource {
21    pub fn from_probes(hour: f64, skies: &[HdrSky], probes: &[Probe]) -> Result<Self, String> {
22        let first = skies.first().ok_or("sky probes need at least one HDR")?;
23        if skies.iter().any(|s| {
24            s.width != first.width
25                || s.height != first.height
26                || s.texels.len() != first.texels.len()
27        }) {
28            return Err("sky probes need matching HDR dimensions".into());
29        }
30        let weights = daylight::probe_weights(hour, probes, skies.len());
31        if weights.iter().sum::<f64>() <= 0.0 {
32            return Err("sky probes have no valid index".into());
33        }
34        let mut texels = vec![[0.0; 4]; first.texels.len()];
35        for (sky, weight) in skies.iter().zip(weights) {
36            for (out, input) in texels.iter_mut().zip(&sky.texels) {
37                for (out_channel, input_channel) in out[..3].iter_mut().zip(&input[..3]) {
38                    *out_channel += *input_channel * weight as f32;
39                }
40            }
41        }
42        for pixel in &mut texels {
43            pixel[3] = 1.0;
44        }
45        Ok(Self::Hdr(HdrSky {
46            width: first.width,
47            height: first.height,
48            texels,
49        }))
50    }
51
52    pub fn radiance(&self, direction: [f32; 3]) -> [f32; 3] {
53        match self {
54            Self::Analytic(model) => model.radiance(direction),
55            Self::Hdr(hdr) => {
56                let d = unit(direction);
57                let u = (d[0].atan2(-d[2]) / TAU + 0.5).rem_euclid(1.0);
58                let v = d[1].clamp(-1.0, 1.0).acos() / PI;
59                let x = u * hdr.width as f32 - 0.5;
60                let y = v * hdr.height as f32 - 0.5;
61                let x0 = x.floor();
62                let y0 = y.floor();
63                let tx = x - x0;
64                let ty = y - y0;
65                let at = |ix: f32, iy: f32| {
66                    let col = (ix as i32).rem_euclid(hdr.width as i32) as usize;
67                    let row = (iy as i32).clamp(0, hdr.height as i32 - 1) as usize;
68                    hdr.texels[row * hdr.width as usize + col]
69                };
70                let a = at(x0, y0);
71                let b = at(x0 + 1.0, y0);
72                let c = at(x0, y0 + 1.0);
73                let d = at(x0 + 1.0, y0 + 1.0);
74                [0, 1, 2].map(|i| {
75                    (a[i] * (1.0 - tx) + b[i] * tx) * (1.0 - ty)
76                        + (c[i] * (1.0 - tx) + d[i] * tx) * ty
77                })
78            }
79        }
80    }
81
82    fn diffuse(&self, direction: [f32; 3]) -> [f32; 3] {
83        match self {
84            Self::Analytic(model) => model.diffuse(direction),
85            Self::Hdr(_) => self.radiance(direction),
86        }
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct SkyLighting {
92    pub sh: [[f32; 3]; 9],
93    pub cube: Vec<Vec<[f32; 4]>>,
94    pub edge: u32,
95    pub mip_count: u32,
96    source: SkySource,
97    sh_next: usize,
98    cube_next: u32,
99}
100
101impl SkyLighting {
102    pub fn new(source: SkySource, edge: u32) -> Self {
103        let edge = edge.max(1).next_power_of_two();
104        let mip_count = edge.ilog2() + 1;
105        let cube = (0..mip_count)
106            .map(|mip| vec![[0.0; 4]; (6 * (edge >> mip).pow(2)) as usize])
107            .collect();
108        Self {
109            sh: [[0.0; 3]; 9],
110            cube,
111            edge,
112            mip_count,
113            source,
114            sh_next: 0,
115            cube_next: 0,
116        }
117    }
118
119    pub fn set_source(&mut self, source: SkySource) {
120        self.source = source;
121        self.sh = [[0.0; 3]; 9];
122        self.sh_next = 0;
123        self.cube_next = 0;
124    }
125
126    pub fn source(&self) -> &SkySource {
127        &self.source
128    }
129
130    pub fn step(&mut self) -> Option<(u32, u32)> {
131        if self.sh_next < SH_SAMPLES {
132            self.sh_chunk();
133        }
134        if self.cube_next >= self.mip_count * 6 {
135            return None;
136        }
137        let mip = self.cube_next / 6;
138        let face = self.cube_next % 6;
139        let texels = self.face(mip, face);
140        let size = self.edge >> mip;
141        let at = (face * size * size) as usize;
142        self.cube[mip as usize][at..at + texels.len()].copy_from_slice(&texels);
143        self.cube_next += 1;
144        Some((mip, face))
145    }
146
147    pub fn settle(&mut self) {
148        while self.sh_next < SH_SAMPLES {
149            self.sh_chunk();
150        }
151        let first = self.cube_next;
152        let mip_count = self.mip_count;
153        let edge = self.edge;
154        let lighting = &*self;
155        let faces: Vec<Vec<(u32, Vec<[f32; 4]>)>> = std::thread::scope(|scope| {
156            let workers: Vec<_> = (0..6)
157                .map(|face| {
158                    scope.spawn(move || {
159                        (0..mip_count)
160                            .filter(|mip| mip * 6 + face >= first)
161                            .map(|mip| (mip, lighting.face(mip, face)))
162                            .collect::<Vec<_>>()
163                    })
164                })
165                .collect();
166            workers
167                .into_iter()
168                .map(|worker| worker.join().expect("sky face worker"))
169                .collect()
170        });
171        for (face, mips) in faces.into_iter().enumerate() {
172            for (mip, texels) in mips {
173                let size = edge >> mip;
174                let at = face * (size * size) as usize;
175                self.cube[mip as usize][at..at + texels.len()].copy_from_slice(&texels);
176            }
177        }
178        self.cube_next = self.mip_count * 6;
179    }
180
181    fn sh_chunk(&mut self) {
182        for index in self.sh_next..(self.sh_next + 256).min(SH_SAMPLES) {
183            let z = 1.0 - 2.0 * (index as f32 + 0.5) / SH_SAMPLES as f32;
184            let angle = index as f32 * 2.399_963_1;
185            let radius = (1.0 - z * z).sqrt();
186            let d = [radius * angle.cos(), z, radius * angle.sin()];
187            let color = self.source.diffuse(d);
188            let basis = sh_basis(d);
189            let scale = 4.0 * PI / SH_SAMPLES as f32;
190            for (l, basis_value) in basis.iter().enumerate() {
191                let convolution = if l == 0 {
192                    PI
193                } else if l < 4 {
194                    2.0 * PI / 3.0
195                } else {
196                    PI / 4.0
197                };
198                for (coefficient, value) in self.sh[l].iter_mut().zip(color) {
199                    *coefficient += value * basis_value * scale * convolution;
200                }
201            }
202        }
203        self.sh_next = (self.sh_next + 256).min(SH_SAMPLES);
204    }
205
206    fn face(&self, mip: u32, face: u32) -> Vec<[f32; 4]> {
207        let size = self.edge >> mip;
208        let roughness = mip as f32 / (self.mip_count - 1).max(1) as f32;
209        let mut texels = Vec::with_capacity((size * size) as usize);
210        for y in 0..size {
211            for x in 0..size {
212                let n = cube_direction(
213                    face,
214                    (x as f32 + 0.5) / size as f32,
215                    (y as f32 + 0.5) / size as f32,
216                );
217                let color = prefilter(&self.source, n, roughness);
218                texels.push([color[0], color[1], color[2], 1.0]);
219            }
220        }
221        texels
222    }
223
224    pub fn complete(&self) -> bool {
225        self.sh_next == SH_SAMPLES && self.cube_next == self.mip_count * 6
226    }
227
228    pub fn irradiance(&self, normal: [f32; 3]) -> [f32; 3] {
229        let basis = sh_basis(unit(normal));
230        [0, 1, 2].map(|channel| {
231            (0..9)
232                .map(|i| self.sh[i][channel] * basis[i])
233                .sum::<f32>()
234                .max(0.0)
235        })
236    }
237}
238
239pub struct SettledSky {
240    pub request: u64,
241    pub lighting: SkyLighting,
242    textures: Option<(wgpu::Texture, wgpu::Texture)>,
243}
244
245struct SettlerState {
246    request: Option<(u64, SkySource)>,
247    result: Option<SettledSky>,
248    busy: bool,
249    closed: bool,
250    built: u64,
251}
252
253struct SettlerShared {
254    state: Mutex<SettlerState>,
255    wake: Condvar,
256}
257
258impl SettlerShared {
259    fn lock(&self) -> MutexGuard<'_, SettlerState> {
260        self.state
261            .lock()
262            .unwrap_or_else(|poisoned| poisoned.into_inner())
263    }
264}
265
266pub struct SkySettler {
267    edge: u32,
268    gpu: Option<(wgpu::Device, wgpu::Queue)>,
269    shared: Arc<SettlerShared>,
270    worker: Option<JoinHandle<()>>,
271}
272
273impl SkySettler {
274    pub fn new(edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) -> Self {
275        Self {
276            edge,
277            gpu,
278            shared: Arc::new(SettlerShared {
279                state: Mutex::new(SettlerState {
280                    request: None,
281                    result: None,
282                    busy: false,
283                    closed: false,
284                    built: 0,
285                }),
286                wake: Condvar::new(),
287            }),
288            worker: None,
289        }
290    }
291
292    pub fn request(&mut self, request: u64, source: SkySource) {
293        if self.worker.is_none() {
294            let shared = self.shared.clone();
295            let edge = self.edge;
296            let gpu = self.gpu.clone();
297            self.worker = Some(
298                std::thread::Builder::new()
299                    .name("pito sky settle".into())
300                    .spawn(move || settle_worker(&shared, edge, gpu))
301                    .expect("spawn the sky settle thread"),
302            );
303        }
304        self.shared.lock().request = Some((request, source));
305        self.shared.wake.notify_all();
306    }
307
308    pub fn cancel(&self) {
309        let mut state = self.shared.lock();
310        state.request = None;
311        state.result = None;
312    }
313
314    pub fn take(&self) -> Option<SettledSky> {
315        self.shared.lock().result.take()
316    }
317
318    pub fn idle(&self) -> bool {
319        let state = self.shared.lock();
320        state.request.is_none() && !state.busy
321    }
322
323    pub fn built(&self) -> u64 {
324        self.shared.lock().built
325    }
326
327    pub fn wait(&self) {
328        let mut state = self.shared.lock();
329        while state.request.is_some() || state.busy {
330            state = self
331                .shared
332                .wake
333                .wait(state)
334                .unwrap_or_else(|poisoned| poisoned.into_inner());
335        }
336    }
337}
338
339impl Drop for SkySettler {
340    fn drop(&mut self) {
341        self.shared.lock().closed = true;
342        self.shared.wake.notify_all();
343        if let Some(worker) = self.worker.take() {
344            let _ = worker.join();
345        }
346    }
347}
348
349fn settle_worker(shared: &SettlerShared, edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) {
350    loop {
351        let (request, source) = {
352            let mut state = shared.lock();
353            loop {
354                if state.closed {
355                    return;
356                }
357                if let Some(next) = state.request.take() {
358                    state.busy = true;
359                    break next;
360                }
361                state = shared
362                    .wake
363                    .wait(state)
364                    .unwrap_or_else(|poisoned| poisoned.into_inner());
365            }
366        };
367        let mut lighting = SkyLighting::new(source, edge);
368        lighting.settle();
369        let textures = gpu.as_ref().map(|(device, queue)| {
370            (
371                sky_hdr(device, queue, &lighting.source),
372                sky_cube(device, queue, &lighting),
373            )
374        });
375        let mut state = shared.lock();
376        state.busy = false;
377        state.built += 1;
378        if state
379            .result
380            .as_ref()
381            .is_none_or(|result| result.request < request)
382        {
383            state.result = Some(SettledSky {
384                request,
385                lighting,
386                textures,
387            });
388        }
389        drop(state);
390        shared.wake.notify_all();
391    }
392}
393
394pub struct SkySteps {
395    settler: SkySettler,
396    pending: Option<(u64, SkySource)>,
397    next: u64,
398    shown: u64,
399}
400
401impl SkySteps {
402    pub fn new(edge: u32, gpu: Option<(wgpu::Device, wgpu::Queue)>) -> Self {
403        Self {
404            settler: SkySettler::new(edge, gpu),
405            pending: None,
406            next: 0,
407            shown: 0,
408        }
409    }
410
411    pub fn request(&mut self, source: SkySource) -> u64 {
412        self.next += 1;
413        self.pending = Some((self.next, source));
414        self.next
415    }
416
417    pub fn settled(&mut self) -> u64 {
418        self.next += 1;
419        self.shown = self.next;
420        self.pending = None;
421        self.settler.cancel();
422        self.next
423    }
424
425    pub fn dispatch(&mut self) {
426        if let Some((request, source)) = self.pending.take() {
427            self.settler.request(request, source);
428        }
429    }
430
431    pub fn take(&mut self) -> Option<SettledSky> {
432        let settled = self.settler.take()?;
433        if settled.request <= self.shown {
434            return None;
435        }
436        self.shown = settled.request;
437        Some(settled)
438    }
439
440    pub fn shown(&self) -> u64 {
441        self.shown
442    }
443
444    pub fn requested(&self) -> u64 {
445        self.next
446    }
447
448    pub fn settling(&self) -> bool {
449        self.pending.is_some() || !self.settler.idle() || self.shown < self.next
450    }
451
452    pub fn built(&self) -> u64 {
453        self.settler.built()
454    }
455
456    pub fn wait(&mut self) {
457        self.dispatch();
458        self.settler.wait();
459    }
460}
461
462fn f16_bytes(texels: &[[f32; 4]]) -> Vec<u8> {
463    let mut bytes = Vec::with_capacity(texels.len() * 8);
464    for pixel in texels {
465        for channel in pixel {
466            bytes.extend_from_slice(&half::f16::from_f32(*channel).to_bits().to_le_bytes());
467        }
468    }
469    bytes
470}
471
472fn hdr_texture(device: &wgpu::Device, source: &SkySource) -> wgpu::Texture {
473    let (width, height) = match source {
474        SkySource::Hdr(hdr) => (hdr.width, hdr.height),
475        _ => (1, 1),
476    };
477    device.create_texture(&wgpu::TextureDescriptor {
478        label: Some("sky HDR"),
479        size: wgpu::Extent3d {
480            width,
481            height,
482            depth_or_array_layers: 1,
483        },
484        mip_level_count: 1,
485        sample_count: 1,
486        dimension: wgpu::TextureDimension::D2,
487        format: wgpu::TextureFormat::Rgba16Float,
488        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
489        view_formats: &[],
490    })
491}
492
493fn sky_hdr(device: &wgpu::Device, queue: &wgpu::Queue, source: &SkySource) -> wgpu::Texture {
494    let texture = hdr_texture(device, source);
495    if let SkySource::Hdr(hdr) = source {
496        queue.write_texture(
497            texture.as_image_copy(),
498            &f16_bytes(&hdr.texels),
499            wgpu::TexelCopyBufferLayout {
500                offset: 0,
501                bytes_per_row: Some(8 * hdr.width),
502                rows_per_image: Some(hdr.height),
503            },
504            texture.size(),
505        );
506    }
507    texture
508}
509
510fn cube_texture(device: &wgpu::Device, lighting: &SkyLighting) -> wgpu::Texture {
511    device.create_texture(&wgpu::TextureDescriptor {
512        label: Some("sky GGX cube"),
513        size: wgpu::Extent3d {
514            width: lighting.edge,
515            height: lighting.edge,
516            depth_or_array_layers: 6,
517        },
518        mip_level_count: lighting.mip_count,
519        sample_count: 1,
520        dimension: wgpu::TextureDimension::D2,
521        format: wgpu::TextureFormat::Rgba16Float,
522        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
523        view_formats: &[],
524    })
525}
526
527fn sky_cube(device: &wgpu::Device, queue: &wgpu::Queue, lighting: &SkyLighting) -> wgpu::Texture {
528    let texture = cube_texture(device, lighting);
529    for (mip, texels) in lighting.cube.iter().enumerate() {
530        let size = lighting.edge >> mip;
531        queue.write_texture(
532            wgpu::TexelCopyTextureInfo {
533                texture: &texture,
534                mip_level: mip as u32,
535                origin: wgpu::Origin3d::ZERO,
536                aspect: wgpu::TextureAspect::All,
537            },
538            &f16_bytes(texels),
539            wgpu::TexelCopyBufferLayout {
540                offset: 0,
541                bytes_per_row: Some(size * 8),
542                rows_per_image: Some(size),
543            },
544            wgpu::Extent3d {
545                width: size,
546                height: size,
547                depth_or_array_layers: 6,
548            },
549        );
550    }
551    texture
552}
553
554fn sh_basis(d: [f32; 3]) -> [f32; 9] {
555    [
556        0.282095,
557        0.488603 * d[1],
558        0.488603 * d[2],
559        0.488603 * d[0],
560        1.092548 * d[0] * d[1],
561        1.092548 * d[1] * d[2],
562        0.315392 * (3.0 * d[2] * d[2] - 1.0),
563        1.092548 * d[0] * d[2],
564        0.546274 * (d[0] * d[0] - d[1] * d[1]),
565    ]
566}
567
568fn prefilter(source: &SkySource, n: [f32; 3], roughness: f32) -> [f32; 3] {
569    if roughness == 0.0 {
570        return source.radiance(n);
571    }
572    let up = if n[1].abs() < 0.99 {
573        [0.0, 1.0, 0.0]
574    } else {
575        [1.0, 0.0, 0.0]
576    };
577    let right = unit(cross(up, n));
578    let up = cross(n, right);
579    let alpha = roughness * roughness;
580    let mut sum = [0.0; 3];
581    let mut weight = 0.0;
582    for i in 0..64_u32 {
583        let xi = (i as f32 + 0.5) / 64.0;
584        let phi = TAU * radical_inverse(i);
585        let cos_theta = ((1.0 - xi) / (1.0 + (alpha * alpha - 1.0) * xi)).sqrt();
586        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
587        let h = add(
588            add(
589                mul(right, sin_theta * phi.cos()),
590                mul(up, sin_theta * phi.sin()),
591            ),
592            mul(n, cos_theta),
593        );
594        let l = unit(add(mul(h, 2.0 * dot(n, h)), mul(n, -1.0)));
595        let ndotl = dot(n, l).max(0.0);
596        if ndotl > 0.0 {
597            let value = source.radiance(l);
598            for (channel, incoming) in sum.iter_mut().zip(value) {
599                *channel += incoming * ndotl;
600            }
601            weight += ndotl;
602        }
603    }
604    sum.map(|v| v / weight.max(1e-6))
605}
606
607fn radical_inverse(value: u32) -> f32 {
608    value.reverse_bits() as f32 * 2.328_306_4e-10
609}
610
611fn cube_direction(face: u32, u: f32, v: f32) -> [f32; 3] {
612    let x = 2.0 * u - 1.0;
613    let y = 1.0 - 2.0 * v;
614    unit(match face {
615        0 => [1.0, y, -x],
616        1 => [-1.0, y, x],
617        2 => [x, 1.0, -y],
618        3 => [x, -1.0, y],
619        4 => [x, y, 1.0],
620        _ => [-x, y, -1.0],
621    })
622}
623
624fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
625    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
626}
627fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
628    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
629}
630fn mul(a: [f32; 3], s: f32) -> [f32; 3] {
631    a.map(|v| v * s)
632}
633fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
634    [
635        a[1] * b[2] - a[2] * b[1],
636        a[2] * b[0] - a[0] * b[2],
637        a[0] * b[1] - a[1] * b[0],
638    ]
639}
640fn unit(a: [f32; 3]) -> [f32; 3] {
641    mul(a, 1.0 / dot(a, a).sqrt().max(1e-8))
642}
643
644pub static SKY_WGSL: LazyLock<String> = LazyLock::new(|| {
645    format!(
646        "{}\n{}",
647        SKY_MODEL_WGSL.replace("@group(1)", "@group(0)"),
648        SKY_PASS_WGSL
649    )
650});
651
652const SKY_PASS_WGSL: &str = r#"
653struct SkyVertex {
654    @builtin(position) position: vec4f,
655    @location(0) uv: vec2f,
656};
657@vertex
658fn sky_vs(@builtin(vertex_index) index: u32) -> SkyVertex {
659    var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
660    var output: SkyVertex;
661    output.position = vec4f(positions[index], 1.0, 1.0);
662    output.uv = positions[index]*vec2f(0.5, -0.5)+vec2f(0.5, 0.5);
663    return output;
664}
665@fragment
666fn sky_fs(input: SkyVertex) -> @location(0) vec4f {
667    let direction = normalize(sky.forward.xyz + sky.right.xyz*(input.uv.x*2.0-1.0) + sky.up.xyz*(1.0-input.uv.y*2.0));
668    return vec4f(sky_radiance(direction), 1.0);
669}
670"#;
671
672#[repr(C)]
673#[derive(Clone, Copy, Pod, Zeroable)]
674struct SkyUniform {
675    sun: [f32; 4],
676    sun_colour_intensity: [f32; 4],
677    ambient_turbidity_albedo: [f32; 4],
678    albedo_mode: [f32; 4],
679    forward: [f32; 4],
680    right: [f32; 4],
681    up: [f32; 4],
682}
683
684pub struct SkyPass {
685    pipeline: wgpu::RenderPipeline,
686    layout: wgpu::BindGroupLayout,
687    uniform: wgpu::Buffer,
688    bind: wgpu::BindGroup,
689    hdr: wgpu::Texture,
690    sampler: wgpu::Sampler,
691    cube: wgpu::Texture,
692    device: wgpu::Device,
693    steps: SkySteps,
694    pub lighting: SkyLighting,
695}
696
697impl SkyPass {
698    pub fn new(
699        device: &wgpu::Device,
700        queue: &wgpu::Queue,
701        color: wgpu::TextureFormat,
702        depth: Option<wgpu::TextureFormat>,
703        source: SkySource,
704        cube_edge: u32,
705    ) -> Self {
706        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
707            label: Some("sky layout"),
708            entries: &[
709                wgpu::BindGroupLayoutEntry {
710                    binding: 0,
711                    visibility: wgpu::ShaderStages::FRAGMENT,
712                    ty: wgpu::BindingType::Buffer {
713                        ty: wgpu::BufferBindingType::Uniform,
714                        has_dynamic_offset: false,
715                        min_binding_size: None,
716                    },
717                    count: None,
718                },
719                wgpu::BindGroupLayoutEntry {
720                    binding: 1,
721                    visibility: wgpu::ShaderStages::FRAGMENT,
722                    ty: wgpu::BindingType::Texture {
723                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
724                        view_dimension: wgpu::TextureViewDimension::D2,
725                        multisampled: false,
726                    },
727                    count: None,
728                },
729                wgpu::BindGroupLayoutEntry {
730                    binding: 2,
731                    visibility: wgpu::ShaderStages::FRAGMENT,
732                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
733                    count: None,
734                },
735            ],
736        });
737        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
738            label: Some("sky shader"),
739            source: wgpu::ShaderSource::Wgsl(SKY_WGSL.as_str().into()),
740        });
741        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
742            label: Some("sky pipeline layout"),
743            bind_group_layouts: &[Some(&layout)],
744            immediate_size: 0,
745        });
746        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
747            label: Some("sky pass"),
748            layout: Some(&pipeline_layout),
749            vertex: wgpu::VertexState {
750                module: &shader,
751                entry_point: Some("sky_vs"),
752                compilation_options: Default::default(),
753                buffers: &[],
754            },
755            primitive: Default::default(),
756            depth_stencil: depth.map(|format| wgpu::DepthStencilState {
757                format,
758                depth_write_enabled: Some(false),
759                depth_compare: Some(wgpu::CompareFunction::LessEqual),
760                stencil: Default::default(),
761                bias: Default::default(),
762            }),
763            multisample: Default::default(),
764            fragment: Some(wgpu::FragmentState {
765                module: &shader,
766                entry_point: Some("sky_fs"),
767                compilation_options: Default::default(),
768                targets: &[Some(wgpu::ColorTargetState {
769                    format: color,
770                    blend: None,
771                    write_mask: wgpu::ColorWrites::ALL,
772                })],
773            }),
774            multiview_mask: None,
775            cache: None,
776        });
777        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
778            label: Some("sky uniform"),
779            size: std::mem::size_of::<SkyUniform>() as u64,
780            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
781            mapped_at_creation: false,
782        });
783        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
784            address_mode_u: wgpu::AddressMode::Repeat,
785            address_mode_v: wgpu::AddressMode::ClampToEdge,
786            mag_filter: wgpu::FilterMode::Linear,
787            min_filter: wgpu::FilterMode::Linear,
788            ..Default::default()
789        });
790        let hdr = hdr_texture(device, &source);
791        let bind = Self::bind(device, &layout, &uniform, &hdr, &sampler);
792        let lighting = SkyLighting::new(source, cube_edge);
793        let cube = cube_texture(device, &lighting);
794        let steps = SkySteps::new(lighting.edge, Some((device.clone(), queue.clone())));
795        let mut pass = Self {
796            pipeline,
797            layout,
798            uniform,
799            bind,
800            hdr,
801            sampler,
802            cube,
803            device: device.clone(),
804            steps,
805            lighting,
806        };
807        pass.set_source(device, queue, pass.lighting.source.clone());
808        pass
809    }
810
811    fn bind(
812        device: &wgpu::Device,
813        layout: &wgpu::BindGroupLayout,
814        uniform: &wgpu::Buffer,
815        hdr: &wgpu::Texture,
816        sampler: &wgpu::Sampler,
817    ) -> wgpu::BindGroup {
818        device.create_bind_group(&wgpu::BindGroupDescriptor {
819            label: Some("sky binding"),
820            layout,
821            entries: &[
822                wgpu::BindGroupEntry {
823                    binding: 0,
824                    resource: uniform.as_entire_binding(),
825                },
826                wgpu::BindGroupEntry {
827                    binding: 1,
828                    resource: wgpu::BindingResource::TextureView(
829                        &hdr.create_view(&Default::default()),
830                    ),
831                },
832                wgpu::BindGroupEntry {
833                    binding: 2,
834                    resource: wgpu::BindingResource::Sampler(sampler),
835                },
836            ],
837        })
838    }
839
840    pub fn set_source(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, source: SkySource) {
841        self.steps.settled();
842        self.hdr = sky_hdr(device, queue, &source);
843        self.bind = Self::bind(
844            device,
845            &self.layout,
846            &self.uniform,
847            &self.hdr,
848            &self.sampler,
849        );
850        self.lighting.set_source(source);
851    }
852
853    pub fn request_source(&mut self, source: SkySource) -> u64 {
854        self.steps.request(source)
855    }
856
857    pub fn swap_settled(&mut self) -> Option<u64> {
858        self.steps.dispatch();
859        let settled = self.steps.take()?;
860        let (hdr, cube) = settled.textures?;
861        self.hdr = hdr;
862        self.cube = cube;
863        self.lighting = settled.lighting;
864        self.bind = Self::bind(
865            &self.device,
866            &self.layout,
867            &self.uniform,
868            &self.hdr,
869            &self.sampler,
870        );
871        Some(settled.request)
872    }
873
874    pub fn settling(&self) -> bool {
875        self.steps.settling()
876    }
877
878    pub fn shown(&self) -> u64 {
879        self.steps.shown()
880    }
881
882    pub fn wait_settled(&mut self) -> Option<u64> {
883        self.steps.wait();
884        self.swap_settled()
885    }
886
887    pub fn update(
888        &mut self,
889        queue: &wgpu::Queue,
890        forward: [f32; 3],
891        right: [f32; 3],
892        up: [f32; 3],
893    ) {
894        self.swap_settled();
895        let (model, mode) = match self.lighting.source() {
896            SkySource::Analytic(model) => (*model, 0.0),
897            SkySource::Hdr(_) => (
898                AnalyticSky {
899                    sun: [0.0, 1.0, 0.0],
900                    sun_colour: [1.0; 3],
901                    sun_intensity: 0.0,
902                    ambient: 0.0,
903                    turbidity: 2.0,
904                    ground_albedo: [0.0; 3],
905                },
906                1.0,
907            ),
908        };
909        let data = SkyUniform {
910            sun: [model.sun[0], model.sun[1], model.sun[2], 0.0],
911            sun_colour_intensity: [
912                model.sun_colour[0],
913                model.sun_colour[1],
914                model.sun_colour[2],
915                model.sun_intensity,
916            ],
917            ambient_turbidity_albedo: [
918                model.ambient,
919                model.turbidity,
920                model.ground_albedo[0],
921                model.ground_albedo[1],
922            ],
923            albedo_mode: [model.ground_albedo[2], mode, 0.0, 0.0],
924            forward: [forward[0], forward[1], forward[2], 0.0],
925            right: [right[0], right[1], right[2], 0.0],
926            up: [up[0], up[1], up[2], 0.0],
927        };
928        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&data));
929        if let Some((mip, face)) = self.lighting.step() {
930            let size = self.lighting.edge >> mip;
931            let start = (face * size * size) as usize;
932            let end = start + (size * size) as usize;
933            let bytes = f16_bytes(&self.lighting.cube[mip as usize][start..end]);
934            queue.write_texture(
935                wgpu::TexelCopyTextureInfo {
936                    texture: &self.cube,
937                    mip_level: mip,
938                    origin: wgpu::Origin3d {
939                        x: 0,
940                        y: 0,
941                        z: face,
942                    },
943                    aspect: wgpu::TextureAspect::All,
944                },
945                &bytes,
946                wgpu::TexelCopyBufferLayout {
947                    offset: 0,
948                    bytes_per_row: Some(size * 8),
949                    rows_per_image: Some(size),
950                },
951                wgpu::Extent3d {
952                    width: size,
953                    height: size,
954                    depth_or_array_layers: 1,
955                },
956            );
957        }
958    }
959
960    pub fn cube_view(&self) -> wgpu::TextureView {
961        self.cube.create_view(&wgpu::TextureViewDescriptor {
962            dimension: Some(wgpu::TextureViewDimension::Cube),
963            ..Default::default()
964        })
965    }
966
967    pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
968        pass.set_pipeline(&self.pipeline);
969        pass.set_bind_group(0, &self.bind, &[]);
970        pass.draw(0..3, 0..1);
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977
978    fn daylight(hour: f64) -> Daylight {
979        Daylight {
980            hour,
981            day: 172.0,
982            latitude: 45.0,
983            heading: 180.0,
984        }
985    }
986
987    fn model(hour: f64) -> AnalyticSky {
988        AnalyticSky::new(
989            daylight(hour),
990            daylight::REFERENCE_HOUR,
991            2.5,
992            [0.2, 0.25, 0.2],
993        )
994    }
995
996    #[test]
997    fn sun_is_brightest_and_night_is_dark() {
998        let noon = model(12.0);
999        let near = noon.radiance(noon.sun);
1000        let away = noon.radiance([-noon.sun[0], noon.sun[1], -noon.sun[2]]);
1001        assert!(near[0] > away[0] * 100.0);
1002        let night = model(0.0);
1003        let above = [0.0, 1.0, 0.0];
1004        assert!(noon.radiance(above)[2] > night.radiance(above)[2] * 5.0);
1005    }
1006
1007    #[test]
1008    fn sh_reconstructs_diffuse_irradiance() {
1009        let source = SkySource::Analytic(model(12.0));
1010        let mut lighting = SkyLighting::new(source.clone(), 2);
1011        while !lighting.complete() {
1012            lighting.step();
1013        }
1014        for normal in [[0.0, 1.0, 0.0], [0.8, 0.6, 0.0], [0.0, 0.5, 0.866_025_4]] {
1015            let mut reference = [0.0; 3];
1016            let count = 16_384;
1017            for i in 0..count {
1018                let y = 1.0 - 2.0 * (i as f32 + 0.5) / count as f32;
1019                let phi = i as f32 * 2.399_963_1;
1020                let r = (1.0 - y * y).sqrt();
1021                let dir = [r * phi.cos(), y, r * phi.sin()];
1022                let weight = dot(dir, normal).max(0.0) * 4.0 * PI / count as f32;
1023                let color = source.diffuse(dir);
1024                for (channel, incoming) in reference.iter_mut().zip(color) {
1025                    *channel += incoming * weight;
1026                }
1027            }
1028            let sh = lighting.irradiance(normal);
1029            for c in 0..3 {
1030                let error = (sh[c] - reference[c]).abs() / reference[c].max(0.01);
1031                assert!(
1032                    error < 0.18,
1033                    "{normal:?} channel {c}: {sh:?} versus {reference:?}"
1034                );
1035            }
1036        }
1037    }
1038
1039    #[test]
1040    fn hdr_blend_and_ggx_cube() {
1041        let red = HdrSky {
1042            width: 2,
1043            height: 1,
1044            texels: vec![[2.0, 0.0, 0.0, 1.0]; 2],
1045        };
1046        let blue = HdrSky {
1047            width: 2,
1048            height: 1,
1049            texels: vec![[0.0, 0.0, 4.0, 1.0]; 2],
1050        };
1051        let probes = [
1052            Probe {
1053                hour: 6.0,
1054                index: 0,
1055            },
1056            Probe {
1057                hour: 18.0,
1058                index: 1,
1059            },
1060        ];
1061        let source = SkySource::from_probes(12.0, &[red, blue], &probes).unwrap();
1062        let color = source.radiance([0.0, 1.0, 0.0]);
1063        assert!((color[0] - 1.0).abs() < 1e-5 && (color[2] - 2.0).abs() < 1e-5);
1064        let mut lighting = SkyLighting::new(source, 4);
1065        while !lighting.complete() {
1066            lighting.step();
1067        }
1068        for mip in &lighting.cube {
1069            for texel in mip {
1070                assert!((texel[0] - 1.0).abs() < 1e-3 && (texel[2] - 2.0).abs() < 1e-3);
1071            }
1072        }
1073    }
1074
1075    fn same_bits(a: &SkyLighting, b: &SkyLighting) -> bool {
1076        let sh = |l: &SkyLighting| {
1077            l.sh.iter()
1078                .flatten()
1079                .map(|v| v.to_bits())
1080                .collect::<Vec<_>>()
1081        };
1082        let cube = |l: &SkyLighting| {
1083            l.cube
1084                .iter()
1085                .flatten()
1086                .flatten()
1087                .map(|v| v.to_bits())
1088                .collect::<Vec<_>>()
1089        };
1090        a.complete() && b.complete() && sh(a) == sh(b) && cube(a) == cube(b)
1091    }
1092
1093    fn stepped(source: SkySource, edge: u32) -> SkyLighting {
1094        let mut lighting = SkyLighting::new(source, edge);
1095        while !lighting.complete() {
1096            lighting.step();
1097        }
1098        lighting
1099    }
1100
1101    fn banded_hdr() -> HdrSky {
1102        let (width, height) = (32, 16);
1103        HdrSky {
1104            width,
1105            height,
1106            texels: (0..width * height)
1107                .map(|i| {
1108                    let (x, y) = (i % width, i / width);
1109                    [
1110                        0.2 + x as f32 * 0.05,
1111                        0.1 + y as f32 * 0.3,
1112                        if (x + y) % 5 == 0 { 6.0 } else { 0.4 },
1113                        1.0,
1114                    ]
1115                })
1116                .collect(),
1117        }
1118    }
1119
1120    #[test]
1121    fn settling_off_thread_equals_stepping_bit_for_bit() {
1122        for source in [
1123            SkySource::Analytic(model(9.5)),
1124            SkySource::Hdr(banded_hdr()),
1125        ] {
1126            let reference = stepped(source.clone(), 16);
1127            let mut settled = SkyLighting::new(source.clone(), 16);
1128            settled.settle();
1129            assert!(same_bits(&reference, &settled));
1130            let mut partly = SkyLighting::new(source.clone(), 16);
1131            for _ in 0..9 {
1132                partly.step();
1133            }
1134            partly.settle();
1135            assert!(same_bits(&reference, &partly));
1136            let mut steps = SkySteps::new(16, None);
1137            let request = steps.request(source);
1138            steps.wait();
1139            let result = steps.take().unwrap();
1140            assert_eq!(result.request, request);
1141            assert!(same_bits(&reference, &result.lighting));
1142            assert!(!steps.settling());
1143        }
1144    }
1145
1146    #[test]
1147    fn twenty_requests_in_one_frame_build_only_the_last() {
1148        let mut steps = SkySteps::new(16, None);
1149        let mut calls = Vec::new();
1150        let mut last = 0;
1151        for index in 0..20 {
1152            let source = SkySource::Analytic(model(6.0 + index as f64 * 0.5));
1153            let started = std::time::Instant::now();
1154            last = steps.request(source);
1155            calls.push(started.elapsed().as_secs_f64() * 1000.0);
1156        }
1157        let started = std::time::Instant::now();
1158        steps.dispatch();
1159        let dispatch = started.elapsed().as_secs_f64() * 1000.0;
1160        assert!(steps.settling());
1161        steps.wait();
1162        let result = steps.take().unwrap();
1163        assert_eq!(result.request, last);
1164        assert_eq!(steps.built(), 1);
1165        assert!(steps.take().is_none());
1166        assert!(same_bits(
1167            &stepped(SkySource::Analytic(model(6.0 + 19.0 * 0.5)), 16),
1168            &result.lighting
1169        ));
1170        let mean = calls.iter().sum::<f64>() / calls.len() as f64;
1171        let most = calls.iter().copied().fold(0.0, f64::max);
1172        println!(
1173            "set_sky on the calling thread: mean {mean:.4} ms, max {most:.4} ms over 20 calls; dispatch {dispatch:.4} ms"
1174        );
1175        assert!(mean < 1.0);
1176        assert!(dispatch < 50.0);
1177    }
1178
1179    #[test]
1180    fn a_scrubber_ends_on_its_newest_hour_and_old_results_never_return() {
1181        let mut steps = SkySteps::new(8, None);
1182        let mut newest = 0;
1183        for frame in 0..30 {
1184            newest = steps.request(SkySource::Analytic(model(8.0 + frame as f64 * 0.25)));
1185            steps.dispatch();
1186            if let Some(result) = steps.take() {
1187                assert!(result.request <= newest);
1188            }
1189        }
1190        steps.wait();
1191        let shown = steps.take().map(|result| result.request);
1192        assert!(shown == Some(newest) || steps.shown() == newest);
1193        assert!(steps.built() <= 30);
1194        let settled = steps.settled();
1195        steps.request(SkySource::Analytic(model(13.0)));
1196        steps.dispatch();
1197        steps.settled();
1198        steps.wait();
1199        assert!(steps.take().is_none());
1200        assert!(settled < steps.shown());
1201    }
1202
1203    #[test]
1204    fn wgsl_parses() {
1205        let module = naga::front::wgsl::parse_str(&SKY_WGSL).unwrap();
1206        naga::valid::Validator::new(
1207            naga::valid::ValidationFlags::all(),
1208            naga::valid::Capabilities::all(),
1209        )
1210        .validate(&module)
1211        .unwrap();
1212    }
1213
1214    #[test]
1215    #[ignore = "needs a GPU; run under pgpu"]
1216    fn gpu_sky_matches_cpu_at_morning_noon_and_evening() {
1217        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
1218        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
1219            label: Some("sky test target"),
1220            size: wgpu::Extent3d {
1221                width: 1,
1222                height: 1,
1223                depth_or_array_layers: 1,
1224            },
1225            mip_level_count: 1,
1226            sample_count: 1,
1227            dimension: wgpu::TextureDimension::D2,
1228            format: wgpu::TextureFormat::Rgba16Float,
1229            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1230            view_formats: &[],
1231        });
1232        let view = texture.create_view(&Default::default());
1233        let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1234            label: Some("sky readback"),
1235            size: 256,
1236            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1237            mapped_at_creation: false,
1238        });
1239        for hour in [8.0, 12.0, 18.0] {
1240            let started = std::time::Instant::now();
1241            let model = model(hour);
1242            let mut pass = SkyPass::new(
1243                &gpu.device,
1244                &gpu.queue,
1245                wgpu::TextureFormat::Rgba16Float,
1246                None,
1247                SkySource::Analytic(model),
1248                2,
1249            );
1250            for direction in [
1251                unit([0.5, 0.7, 0.3]),
1252                [0.0, 1.0, 0.0],
1253                unit([-0.6, 0.4, 0.6]),
1254            ] {
1255                pass.update(&gpu.queue, direction, [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1256                let mut encoder = gpu.device.create_command_encoder(&Default::default());
1257                {
1258                    let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1259                        multiview_mask: None,
1260                        label: Some("sky test"),
1261                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1262                            view: &view,
1263                            resolve_target: None,
1264                            ops: wgpu::Operations {
1265                                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1266                                store: wgpu::StoreOp::Store,
1267                            },
1268                            depth_slice: None,
1269                        })],
1270                        depth_stencil_attachment: None,
1271                        timestamp_writes: None,
1272                        occlusion_query_set: None,
1273                    });
1274                    pass.draw(&mut render);
1275                }
1276                encoder.copy_texture_to_buffer(
1277                    texture.as_image_copy(),
1278                    wgpu::TexelCopyBufferInfo {
1279                        buffer: &readback,
1280                        layout: wgpu::TexelCopyBufferLayout {
1281                            offset: 0,
1282                            bytes_per_row: Some(256),
1283                            rows_per_image: Some(1),
1284                        },
1285                    },
1286                    texture.size(),
1287                );
1288                gpu.queue.submit(Some(encoder.finish()));
1289                let (send, receive) = std::sync::mpsc::channel();
1290                let slice = readback.slice(..);
1291                slice.map_async(wgpu::MapMode::Read, move |result| {
1292                    let _ = send.send(result);
1293                });
1294                gpu.device
1295                    .poll(wgpu::PollType::wait_indefinitely())
1296                    .unwrap();
1297                receive.recv().unwrap().unwrap();
1298                let mapped = slice.get_mapped_range().expect("a mapped range");
1299                let actual = [0, 1, 2].map(|i| {
1300                    half::f16::from_bits(u16::from_le_bytes([mapped[2 * i], mapped[2 * i + 1]]))
1301                        .to_f32()
1302                });
1303                drop(mapped);
1304                readback.unmap();
1305                let expected = model.radiance(direction);
1306                for c in 0..3 {
1307                    assert!(
1308                        (actual[c] - expected[c]).abs() <= expected[c].max(0.01) * 0.04,
1309                        "{hour}: {actual:?} versus {expected:?}"
1310                    );
1311                }
1312                println!("sky GPU {hour:02.0}:00 {direction:?}: {actual:?}");
1313            }
1314            println!(
1315                "sky GPU {hour:02.0}:00 headless test wall time: {:.3} ms",
1316                started.elapsed().as_secs_f64() * 1000.0
1317            );
1318        }
1319    }
1320}