312 lines
10 KiB
Rust
312 lines
10 KiB
Rust
//! Runs the `resolved` binary on the example scenes.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn repo() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
|
|
}
|
|
|
|
fn out_dir() -> PathBuf {
|
|
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("cli");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
dir
|
|
}
|
|
|
|
/// Runs the binary from the repository root with `ASTROKIT_KERNELS` cleared, so the
|
|
/// kernel search falls back the way it does for someone who has not set it.
|
|
fn resolved(args: &[&str]) -> std::process::Output {
|
|
Command::new(env!("CARGO_BIN_EXE_resolved"))
|
|
.args(args)
|
|
.current_dir(repo())
|
|
.env_remove("ASTROKIT_KERNELS")
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
/// Renders `scene` on `adapter` to a 16-bit PNG and returns its pixels as DN (the
|
|
/// file holds 12-bit DN shifted up by 4).
|
|
fn render(scene: &str, adapter: &str) -> image::ImageBuffer<image::Rgb<u16>, Vec<u16>> {
|
|
render_at(scene, adapter, "L0")
|
|
}
|
|
|
|
/// A data directory whose Earth images and Moon map are small and uniform, for tests at
|
|
/// lighting L1 that do not need the real ones: loading those (16384 x 8192 each) takes
|
|
/// seconds in a debug build, and they are only there once fetched.
|
|
fn gray_earth() -> PathBuf {
|
|
static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
|
DIR.get_or_init(|| {
|
|
let dir = out_dir().join("gray-data");
|
|
std::fs::create_dir_all(dir.join("earth")).unwrap();
|
|
let image = image::RgbImage::from_pixel(64, 32, image::Rgb([128; 3]));
|
|
for month in 1..=12 {
|
|
image
|
|
.save(dir.join(format!("earth/bmng-2004-{month:02}.jpg")))
|
|
.unwrap();
|
|
}
|
|
// A uniform Moon: I/F 0.06 (16-bit values of I/F x 100,000).
|
|
std::fs::create_dir_all(dir.join("moon")).unwrap();
|
|
image::ImageBuffer::<image::Luma<u16>, _>::from_pixel(64, 32, image::Luma([6000]))
|
|
.save(dir.join("moon/wac-hapke-643.png"))
|
|
.unwrap();
|
|
dir
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
/// [`render`] at lighting tier `lighting`, with Earth's images from [`gray_earth`].
|
|
fn render_at(
|
|
scene: &str,
|
|
adapter: &str,
|
|
lighting: &str,
|
|
) -> image::ImageBuffer<image::Rgb<u16>, Vec<u16>> {
|
|
let data = gray_earth();
|
|
let out = out_dir().join(format!(
|
|
"{}-{lighting}-{adapter}.png",
|
|
Path::new(scene).file_stem().unwrap().to_str().unwrap()
|
|
));
|
|
let preview = out.with_extension("preview.png");
|
|
let result = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
scene,
|
|
"--adapter",
|
|
adapter,
|
|
"--lighting",
|
|
lighting,
|
|
"--data",
|
|
data.to_str().unwrap(),
|
|
"--out",
|
|
out.to_str().unwrap(),
|
|
"--preview",
|
|
preview.to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
assert!(preview.is_file());
|
|
let mut pixels = image::open(&out).unwrap().into_rgb16();
|
|
for p in pixels.pixels_mut() {
|
|
assert!(p.0.iter().all(|v| v & 0xf == 0), "low 4 bits are padding");
|
|
p.0 = p.0.map(|v| v >> 4);
|
|
}
|
|
pixels
|
|
}
|
|
|
|
#[test]
|
|
fn example_scene_on_gpu_and_cpu() {
|
|
let gpu = render("examples/scenes/boxwing.toml", "gpu");
|
|
let cpu = render("examples/scenes/boxwing.toml", "cpu");
|
|
assert_eq!(gpu.dimensions(), (1920, 1080));
|
|
// The image center is on the sunlit gold bus: brighter in red than in blue.
|
|
let [r, _, b] = gpu.get_pixel(960, 540).0;
|
|
assert!(r > 500 && r > 2 * b, "{r} {b}");
|
|
// Above the limb is empty space, which reads the 64 DN bias; below it, Earth's
|
|
// dayside, lit and blue.
|
|
assert_eq!(gpu.get_pixel(0, 0).0, [64; 3]);
|
|
let [r, g, b] = gpu.get_pixel(0, 1079).0;
|
|
assert!(r > 64 && g > r && b > g, "{r} {g} {b}");
|
|
let differing = gpu
|
|
.pixels()
|
|
.zip(cpu.pixels())
|
|
.filter(|(a, b)| a.0.iter().zip(b.0).any(|(x, y)| x.abs_diff(y) > 1))
|
|
.count();
|
|
// Only pixels on silhouette edges may land differently.
|
|
assert!(
|
|
differing < 500,
|
|
"{differing} pixels differ between GPU and CPU"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pretty_previews_lift_the_darks_and_leave_space_black() {
|
|
let preview = |pretty: bool| {
|
|
let path = out_dir().join(format!("preview-pretty-{pretty}.png"));
|
|
let path_str = path.to_str().unwrap();
|
|
let mut args = vec![
|
|
"render",
|
|
"--scene",
|
|
"examples/scenes/boxwing.toml",
|
|
"--preview",
|
|
path_str,
|
|
];
|
|
if pretty {
|
|
args.push("--pretty");
|
|
}
|
|
let result = resolved(&args);
|
|
assert!(result.status.success(), "{result:?}");
|
|
image::open(&path).unwrap().into_rgb8()
|
|
};
|
|
let (raw, pretty) = (preview(false), preview(true));
|
|
// Space is the bias, black either way.
|
|
assert_eq!(raw.get_pixel(0, 0).0, [0; 3]);
|
|
assert_eq!(pretty.get_pixel(0, 0).0, [0; 3]);
|
|
// Earth's dayside below the limb, well short of saturation: brighter when pretty.
|
|
let (r, p) = (raw.get_pixel(0, 1079).0, pretty.get_pixel(0, 1079).0);
|
|
assert!((0..3).all(|k| p[k] > r[k]), "raw {r:?}, pretty {p:?}");
|
|
}
|
|
|
|
#[cfg(feature = "astrokit")]
|
|
#[test]
|
|
fn lunar_eclipse_darkens_the_moon() {
|
|
// Mid-totality: at L1 the whole Moon is in Earth's umbra and reads the bias. At L0
|
|
// there are no eclipses on the bodies and it is a full Moon.
|
|
let scene = "examples/scenes/lunar-eclipse.toml";
|
|
let l0 = render_at(scene, "gpu", "L0");
|
|
let l1 = render_at(scene, "gpu", "L1");
|
|
assert!(
|
|
l0.get_pixel(960, 540).0.iter().all(|&v| v > 150),
|
|
"{:?}",
|
|
l0.get_pixel(960, 540)
|
|
);
|
|
assert!(
|
|
l1.pixels().all(|p| p.0 == [64; 3]),
|
|
"every pixel at the bias"
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "astrokit")]
|
|
#[test]
|
|
fn solar_eclipse_puts_the_umbra_at_greatest_eclipse() {
|
|
// The camera looks straight down at the point of greatest eclipse from NASA's
|
|
// bulletin (65.2 N 25.2 W, 17:46 UTC): at L1 it is in the Moon's umbra and reads the
|
|
// bias, and the image corner, about 3500 km away in the penumbra, is brighter. At L0
|
|
// the center is lit.
|
|
let scene = "examples/scenes/solar-eclipse.toml";
|
|
let l0 = render_at(scene, "gpu", "L0");
|
|
let l1 = render_at(scene, "gpu", "L1");
|
|
assert!(
|
|
l0.get_pixel(960, 540).0.iter().all(|&v| v > 100),
|
|
"{:?}",
|
|
l0.get_pixel(960, 540)
|
|
);
|
|
assert_eq!(l1.get_pixel(960, 540).0, [64; 3]);
|
|
let corner = l1.get_pixel(40, 1040).0;
|
|
assert!(corner.iter().all(|&v| v > 70), "{corner:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn lighting_l1_names_a_missing_moon_map() {
|
|
// Earth's images but no Moon map: the error names the file and the script.
|
|
let dir = out_dir().join("earth-only");
|
|
std::fs::create_dir_all(dir.join("earth")).unwrap();
|
|
std::fs::copy(
|
|
gray_earth().join("earth/bmng-2004-07.jpg"),
|
|
dir.join("earth/bmng-2004-07.jpg"),
|
|
)
|
|
.unwrap();
|
|
let result = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
"examples/scenes/landsat8.toml",
|
|
"--lighting",
|
|
"L1",
|
|
"--data",
|
|
dir.to_str().unwrap(),
|
|
"--preview",
|
|
"x.png",
|
|
]);
|
|
assert!(!result.status.success());
|
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
|
assert!(
|
|
stderr.contains("moon/wac-hapke-643.png") && stderr.contains("fetch-moon.py"),
|
|
"{stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_are_reported() {
|
|
let missing = resolved(&["render", "--scene", "no/such.toml", "--preview", "x.png"]);
|
|
assert!(!missing.status.success());
|
|
assert!(String::from_utf8_lossy(&missing.stderr).contains("no/such.toml"));
|
|
|
|
let idle = resolved(&["render", "--scene", "examples/scenes/boxwing.toml"]);
|
|
assert!(!idle.status.success());
|
|
assert!(String::from_utf8_lossy(&idle.stderr).contains("nothing to do"));
|
|
}
|
|
|
|
#[cfg(feature = "astrokit")]
|
|
#[test]
|
|
fn missing_kernels_say_where_they_looked() {
|
|
let result = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
"examples/scenes/full-moon.toml",
|
|
"--kernels",
|
|
"no/such/dir",
|
|
"--preview",
|
|
"x.png",
|
|
]);
|
|
assert!(!result.status.success());
|
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
|
assert!(
|
|
stderr.contains("--kernels") && stderr.contains("no/such/dir"),
|
|
"{stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lighting_l1_says_what_it_is_missing() {
|
|
// Without an epoch or an earth_month, L1 cannot pick a month: boxwing.toml with its
|
|
// [appearance] removed.
|
|
let scene = out_dir().join("no-month.toml");
|
|
let models = repo().join("examples/models").canonicalize().unwrap();
|
|
let text = std::fs::read_to_string(repo().join("examples/scenes/boxwing.toml"))
|
|
.unwrap()
|
|
.replace("../models", models.to_str().unwrap())
|
|
.replace("[appearance]\nearth_month = 7\n", "");
|
|
assert!(!text.contains("earth_month"));
|
|
std::fs::write(&scene, text).unwrap();
|
|
let no_month = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
scene.to_str().unwrap(),
|
|
"--lighting",
|
|
"L1",
|
|
"--preview",
|
|
"x.png",
|
|
]);
|
|
assert!(!no_month.status.success());
|
|
let stderr = String::from_utf8_lossy(&no_month.stderr);
|
|
assert!(stderr.contains("earth_month"), "{stderr}");
|
|
|
|
// With a month but no data: the error names the file and the fetch script.
|
|
let no_data = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
"examples/scenes/boxwing.toml",
|
|
"--lighting",
|
|
"L1",
|
|
"--data",
|
|
"no/such/dir",
|
|
"--preview",
|
|
"x.png",
|
|
]);
|
|
assert!(!no_data.status.success());
|
|
let stderr = String::from_utf8_lossy(&no_data.stderr);
|
|
assert!(
|
|
stderr.contains("bmng-2004-07.jpg") && stderr.contains("fetch-earth.sh"),
|
|
"{stderr}"
|
|
);
|
|
|
|
let unknown = resolved(&[
|
|
"render",
|
|
"--scene",
|
|
"examples/scenes/boxwing.toml",
|
|
"--lighting",
|
|
"L7",
|
|
"--preview",
|
|
"x.png",
|
|
]);
|
|
assert!(!unknown.status.success());
|
|
}
|
|
|
|
#[test]
|
|
fn adapters_lists_something() {
|
|
let result = resolved(&["adapters"]);
|
|
assert!(result.status.success());
|
|
assert!(!result.stdout.is_empty());
|
|
}
|