diff --git a/Cargo.toml b/Cargo.toml index 9de802e..095317c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,9 @@ resolved = { path = "crates/resolved" } # otherwise slow enough to dominate the test suite. Our own crates stay unoptimized. [profile.dev.package."*"] opt-level = 2 + +# Tests build our own crates at opt-level 1: with their f64 reference images and +# quadratures unoptimized, the suite took 61 s instead of 23. `cargo build` and +# `cargo run` keep the dev profile. +[profile.test] +opt-level = 1 diff --git a/crates/resolved/tests/analytic.rs b/crates/resolved/tests/gpu/analytic.rs similarity index 99% rename from crates/resolved/tests/analytic.rs rename to crates/resolved/tests/gpu/analytic.rs index 51b8f63..5e07eea 100644 --- a/crates/resolved/tests/analytic.rs +++ b/crates/resolved/tests/gpu/analytic.rs @@ -1,7 +1,7 @@ //! Rendered results against closed-form answers, through the public renderer, on the //! GPU and on the CPU (lavapipe). -mod common; +use crate::common; use resolved::bodies::{Appearance, MOON_RADIUS}; use resolved::gpu::AdapterChoice; diff --git a/crates/resolved/tests/bodies.rs b/crates/resolved/tests/gpu/bodies.rs similarity index 99% rename from crates/resolved/tests/bodies.rs rename to crates/resolved/tests/gpu/bodies.rs index 47c4c53..d37c72a 100644 --- a/crates/resolved/tests/bodies.rs +++ b/crates/resolved/tests/gpu/bodies.rs @@ -1,7 +1,7 @@ //! The bodies pass against closed-form geometry and radiometry, on the GPU and on the //! CPU (lavapipe). -mod common; +use crate::common; use approx::assert_relative_eq; use resolved::bodies::{Appearance, EARTH_EQUATORIAL_RADIUS, EARTH_POLAR_RADIUS, MOON_RADIUS}; diff --git a/crates/resolved/tests/common/mod.rs b/crates/resolved/tests/gpu/common/mod.rs similarity index 91% rename from crates/resolved/tests/common/mod.rs rename to crates/resolved/tests/gpu/common/mod.rs index 2a87268..615ac6f 100644 --- a/crates/resolved/tests/common/mod.rs +++ b/crates/resolved/tests/gpu/common/mod.rs @@ -4,15 +4,12 @@ //! Set `RESOLVED_TEST_IMAGES=1` and each test writes PNGs to //! `target/tmp/test-images/`, one per test case and device. -// Each test binary compiles this module and uses only some of it. -#![allow(dead_code)] - use std::path::PathBuf; use std::sync::OnceLock; use resolved::gpu::{AdapterChoice, GpuContext}; use resolved::radiometry::{Radiometry, AU}; -use resolved::{CameraConfig, MoonTexture}; +use resolved::{CameraConfig, Intrinsics, MoonTexture}; use wgpu::AdapterInfo; /// A Moon map of one value: normalized reflectance 0.0625, exact in f16, which is a @@ -22,6 +19,13 @@ pub fn uniform_moon() -> std::sync::Arc { std::sync::Arc::new(MoonTexture::from_reflectance(1, 1, vec![0.0625]).unwrap()) } +/// The example camera's intrinsics, built once: tests that trace a ray per pixel would +/// otherwise build the example camera for each one. +pub fn example_intrinsics() -> Intrinsics { + static K: OnceLock = OnceLock::new(); + *K.get_or_init(|| CameraConfig::example().intrinsics()) +} + /// Held by any test that creates or drops a renderer, so only one does at a time. /// /// Creating and dropping Vulkan devices on several threads at once crashes or hangs diff --git a/crates/resolved/tests/earth_texture.rs b/crates/resolved/tests/gpu/earth_texture.rs similarity index 99% rename from crates/resolved/tests/earth_texture.rs rename to crates/resolved/tests/gpu/earth_texture.rs index 3453f4d..357a2a1 100644 --- a/crates/resolved/tests/earth_texture.rs +++ b/crates/resolved/tests/gpu/earth_texture.rs @@ -1,7 +1,7 @@ //! Earth drawn from a texture: lookup by latitude and longitude, Earth's rotation, the //! 180 degree seam and minification, on the GPU and on the CPU (lavapipe). -mod common; +use crate::common; use resolved::bodies::Appearance; use resolved::gpu::AdapterChoice; diff --git a/crates/resolved/tests/earthshine.rs b/crates/resolved/tests/gpu/earthshine.rs similarity index 99% rename from crates/resolved/tests/earthshine.rs rename to crates/resolved/tests/gpu/earthshine.rs index 9d4fc95..3867f39 100644 --- a/crates/resolved/tests/earthshine.rs +++ b/crates/resolved/tests/gpu/earthshine.rs @@ -1,7 +1,7 @@ //! Light from Earth and the Moon on targets at lighting L1 (Earthshine and Moonshine), //! on the GPU and on the CPU (lavapipe), against a quadrature over each body in f64. -mod common; +use crate::common; use std::f64::consts::{PI, TAU}; use std::io::Cursor; diff --git a/crates/resolved/tests/eclipses.rs b/crates/resolved/tests/gpu/eclipses.rs similarity index 92% rename from crates/resolved/tests/eclipses.rs rename to crates/resolved/tests/gpu/eclipses.rs index a62f193..ade23c9 100644 --- a/crates/resolved/tests/eclipses.rs +++ b/crates/resolved/tests/gpu/eclipses.rs @@ -3,7 +3,7 @@ //! `eclipse::sun_visible_light` (the limb-darkened disk, per channel) at each pixel's //! surface point. -mod common; +use crate::common; use std::sync::Arc; @@ -78,7 +78,7 @@ fn hit( radius: f64, flattening: f64, ) -> Option> { - let k = CameraConfig::example().intrinsics(); + let k = common::example_intrinsics(); let d = camera.attitude * k.ray(&Vector2::new(u as f64 + 0.5, v as f64 + 0.5)); let stretch = Vector3::new(1.0, 1.0, flattening); let o = (camera.position - center).component_mul(&stretch); @@ -191,20 +191,29 @@ fn tally(eclipsed: &[[f32; 4]], clear: &[[f32; 4]], expected: &[Option<[f64; 4]> t } -/// `clear`, the Moon at L0, with each [`sampled`] pixel of the Moon scaled by the phase -/// factor L1 gives the uniform map there (`moon::phase_factor`): the map's albedo is -/// 0.125, as L0's is. -fn with_phase(clear: &[[f32; 4]], camera: &Pose, moon: Vector3) -> Vec<[f32; 4]> { - let mut out = clear.to_vec(); - for (u, v) in sampled() { - if let Some(p) = hit(camera, u, v, moon, MOON_RADIUS, 1.0) { +/// The phase factor L1 gives the uniform map (`moon::phase_factor`) at each [`sampled`] +/// pixel where the Moon is: the map's albedo is 0.125, as L0's is, so this is all that +/// separates L1 from L0 outside the shadow. Computed once per scene, as [`expected`]. +fn phase_factors(camera: &Pose, moon: Vector3) -> Vec> { + expected( + |u, v| hit(camera, u, v, moon, MOON_RADIUS, 1.0), + |p| { let (to_sun, to_camera) = ((SUN - p).normalize(), (camera.position - p).normalize()); let phase = to_sun .cross(&to_camera) .norm() .atan2(to_sun.dot(&to_camera)); - let factor = moon::phase_factor(phase) as f32; - out[v * 1920 + u] = clear[v * 1920 + u].map(|c| c * factor); + [moon::phase_factor(phase); 4] + }, + ) +} + +/// `clear`, the Moon at L0, with each [`sampled`] pixel scaled by its phase factor. +fn with_phase(clear: &[[f32; 4]], factors: &[Option<[f64; 4]>]) -> Vec<[f32; 4]> { + let mut out = clear.to_vec(); + for ((u, v), factor) in sampled().zip(factors) { + if let Some(f) = factor { + out[v * 1920 + u] = clear[v * 1920 + u].map(|c| c * f[0] as f32); } } out @@ -229,15 +238,16 @@ fn earth_shadows_the_moon() { |u, v| hit(&camera, u, v, moon, MOON_RADIUS, 1.0), |p| sun_visible_light(p, SUN, &earth, limb()), ); - (label, moon, camera, want) + let phase = phase_factors(&camera, moon); + (label, moon, camera, want, phase) }); let _devices = common::device_lock(); for adapter in [AdapterChoice::Gpu, AdapterChoice::Cpu] { let mut l0 = renderer(&adapter, LightingTier::L0); let mut l1 = renderer(&adapter, LightingTier::L1); let name = l1.adapter_info().name.clone(); - for (label, moon, camera, want) in &cases { - let clear = with_phase(&radiance(&mut l0, &scene(*moon), camera), camera, *moon); + for (label, moon, camera, want, phase) in &cases { + let clear = with_phase(&radiance(&mut l0, &scene(*moon), camera), phase); let eclipsed = radiance(&mut l1, &scene(*moon), camera); common::save( &format!("eclipse-lunar-{label}"), diff --git a/crates/resolved/tests/gpu_context.rs b/crates/resolved/tests/gpu/gpu_context.rs similarity index 99% rename from crates/resolved/tests/gpu_context.rs rename to crates/resolved/tests/gpu/gpu_context.rs index 2f045c1..7c9425d 100644 --- a/crates/resolved/tests/gpu_context.rs +++ b/crates/resolved/tests/gpu/gpu_context.rs @@ -3,7 +3,7 @@ //! Needs a Vulkan GPU and Mesa's lavapipe (`vulkan-swrast` on Arch, //! `mesa-vulkan-drivers` on Debian and Ubuntu). -mod common; +use crate::common; use resolved::gpu::{AdapterChoice, GpuContext}; use wgpu::util::DeviceExt; diff --git a/crates/resolved/tests/gpu/main.rs b/crates/resolved/tests/gpu/main.rs new file mode 100644 index 0000000..426ea28 --- /dev/null +++ b/crates/resolved/tests/gpu/main.rs @@ -0,0 +1,17 @@ +//! The integration tests, most of them on the GPU and on the CPU (lavapipe), in one +//! binary: one process opens the devices once, and an edit relinks one test binary +//! instead of eleven. + +mod common; + +mod analytic; +mod bodies; +mod earth_texture; +mod earthshine; +mod eclipses; +mod gpu_context; +mod mesh; +mod moon_texture; +mod renderer; +mod sensor; +mod shadows; diff --git a/crates/resolved/tests/mesh.rs b/crates/resolved/tests/gpu/mesh.rs similarity index 99% rename from crates/resolved/tests/mesh.rs rename to crates/resolved/tests/gpu/mesh.rs index 8cec57d..7be543a 100644 --- a/crates/resolved/tests/mesh.rs +++ b/crates/resolved/tests/gpu/mesh.rs @@ -1,7 +1,7 @@ //! The mesh pass over the bodies pass: shading, silhouette, occlusion in both //! directions and eclipse, on the GPU and on the CPU (lavapipe). -mod common; +use crate::common; use std::io::Cursor; diff --git a/crates/resolved/tests/moon_texture.rs b/crates/resolved/tests/gpu/moon_texture.rs similarity index 99% rename from crates/resolved/tests/moon_texture.rs rename to crates/resolved/tests/gpu/moon_texture.rs index 88813fb..97d770a 100644 --- a/crates/resolved/tests/moon_texture.rs +++ b/crates/resolved/tests/gpu/moon_texture.rs @@ -2,7 +2,7 @@ //! attitude, its color, the 180 degree seam and minification, and the light it sheds //! read from the map, on the GPU and on the CPU (lavapipe). -mod common; +use crate::common; use std::io::Cursor; use std::sync::Arc; @@ -176,7 +176,7 @@ fn the_moons_color_comes_from_its_spectrum() { /// Where the line of sight through the center of pixel (u, v) meets the Moon. fn point_on_the_moon(camera: &Pose, u: usize, v: usize) -> Option> { - let k = CameraConfig::example().intrinsics(); + let k = common::example_intrinsics(); let d = camera.attitude * k.ray(&Vector2::new(u as f64 + 0.5, v as f64 + 0.5)); let o = camera.position - MOON; let (b, c) = (o.dot(&d), o.norm_squared() - MOON_RADIUS * MOON_RADIUS); diff --git a/crates/resolved/tests/renderer.rs b/crates/resolved/tests/gpu/renderer.rs similarity index 99% rename from crates/resolved/tests/renderer.rs rename to crates/resolved/tests/gpu/renderer.rs index dff06c0..902ca58 100644 --- a/crates/resolved/tests/renderer.rs +++ b/crates/resolved/tests/gpu/renderer.rs @@ -1,6 +1,6 @@ //! The public renderer API end to end, on the GPU and on the CPU (lavapipe). -mod common; +use crate::common; use std::io::Cursor; use std::sync::Arc; diff --git a/crates/resolved/tests/sensor.rs b/crates/resolved/tests/gpu/sensor.rs similarity index 99% rename from crates/resolved/tests/sensor.rs rename to crates/resolved/tests/gpu/sensor.rs index f216cb1..533c435 100644 --- a/crates/resolved/tests/sensor.rs +++ b/crates/resolved/tests/gpu/sensor.rs @@ -1,7 +1,7 @@ //! The sensor pass against the CPU reference (`Radiometry::electrons` then //! `Detector::dn`), on the GPU and on the CPU (lavapipe). -mod common; +use crate::common; use resolved::bodies::Appearance; use resolved::gpu::bodies::{BodiesPass, BodiesUniforms, BodyRadiometry}; diff --git a/crates/resolved/tests/shadows.rs b/crates/resolved/tests/gpu/shadows.rs similarity index 99% rename from crates/resolved/tests/shadows.rs rename to crates/resolved/tests/gpu/shadows.rs index 425b840..d1eca01 100644 --- a/crates/resolved/tests/shadows.rs +++ b/crates/resolved/tests/gpu/shadows.rs @@ -1,7 +1,7 @@ //! Shadows on targets at lighting L1, on the GPU and on the CPU (lavapipe): where they //! fall, how wide their penumbra is, and that surfaces do not shadow themselves. -mod common; +use crate::common; use std::io::Cursor; use std::sync::Arc; diff --git a/docs/optics.md b/docs/optics.md index 03c0e9d..9d101a3 100644 --- a/docs/optics.md +++ b/docs/optics.md @@ -444,7 +444,7 @@ It falls from 1 at full phase to 0.61 at 60 degrees and 0.11 at 120 degrees. Up close it is not exact: the near side of the sphere is nearer the camera than its center, so it counts for more. At 40,000 km from the Moon (R/D = 0.043), the real total is 3 % above the formula. The renderer's image agrees -with the exact surface integral to 0.03 % or better (`tests/analytic.rs`). +with the exact surface integral to 0.03 % or better (`tests/gpu/analytic.rs`). This gets the overall brightness and the phase (how much of the disk is lit) right. It misses some things that are visible in real images: @@ -607,7 +607,7 @@ Two choices turn the map into something the renderer can shade: Light the Moon sheds on targets (Moonshine) reads the same map, with the phase factor for the angle between the Sun and the target. -Tests (`tests/moon_texture.rs`), with small synthetic maps, on the GPU and +Tests (`tests/gpu/moon_texture.rs`), with small synthetic maps, on the GPU and lavapipe: the pixel under the camera reads the texel at its latitude and longitude, and moves when the Moon's attitude does; the color is the spectrum's; a uniform map draws exactly the Moon a flat albedo draws times the phase @@ -815,7 +815,7 @@ proportion to the surface's tilt. The lookup is made from a point 1.5 texels off the surface, toward the Sun. And a blocker has to be more than 2 texels nearer the Sun to count. -Tests (`tests/shadows.rs`) check, on the GPU and lavapipe: +Tests (`tests/gpu/shadows.rs`) check, on the GPU and lavapipe: - A plate under a 2 m square: the shadow reads zero direct light, and its edge falls where the square's edge projects, to within one texel. @@ -916,7 +916,7 @@ pixels at the shadow's edges came out wrong: a point the 64-bit model puts fully in the umbra read slightly lit, and one just outside the shadow slightly dimmed. -Tests (`tests/eclipses.rs`), on the GPU and lavapipe, compare each pixel of +Tests (`tests/gpu/eclipses.rs`), on the GPU and lavapipe, compare each pixel of Earth and the Moon against the 64-bit model at that pixel's surface point: the Moon entirely in Earth's umbra, the Moon across the umbra's edge, a total solar eclipse and an annular one. Pixels the model puts fully in shadow @@ -1005,7 +1005,7 @@ side is in view, so Earthshine is zero there without being told. In the penumbra the Sun is partly hidden but a crescent of lit ground is still in view, and that light still arrives. -Tests (`tests/earthshine.rs`) compare a plate's brightness on the GPU and +Tests (`tests/gpu/earthshine.rs`) compare a plate's brightness on the GPU and lavapipe against the same sum done in 64-bit floating point with 90 times as many cells: over the subsolar point at 500 km facing down, sideways and up; toward the terminator; in Earth's shadow; at GEO; 100 km above the Moon at diff --git a/readme.md b/readme.md index 37e8e1f..d55130f 100644 --- a/readme.md +++ b/readme.md @@ -217,7 +217,7 @@ writes a PNG per device to `target/tmp/test-images/`, scaled so a white surface facing the Sun is full white: ```sh -RESOLVED_TEST_IMAGES=1 cargo test --test bodies --test mesh +RESOLVED_TEST_IMAGES=1 cargo test --test gpu -- bodies:: mesh:: ``` ## License