Added benchmarks and fixed a parallelism bug

This commit is contained in:
connor committed 2026-10-14 19:12:48 -04:00
1 parent d61552c33c
commit d08bc33e8d
19 files changed
+758 -27

No files matched your search

Generated
+299
View File
@@ -8,6 +8,24 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@@ -23,6 +41,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]] [[package]]
name = "anise" name = "anise"
version = "0.10.6" version = "0.10.6"
@@ -226,6 +250,22 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.5" version = "1.0.5"
@@ -238,6 +278,33 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.7" version = "4.6.7"
@@ -325,6 +392,66 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "criterion"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.4" version = "0.2.4"
@@ -380,6 +507,12 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.3" version = "1.0.3"
@@ -395,6 +528,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.10" version = "1.1.10"
@@ -643,6 +782,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@@ -1099,6 +1247,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "5.4.0" version = "5.4.0"
@@ -1121,6 +1275,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.18.0" version = "0.18.0"
@@ -1167,6 +1331,34 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -1289,6 +1481,26 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -1298,6 +1510,35 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
version = "1.1.0" version = "1.1.0"
@@ -1312,6 +1553,7 @@ dependencies = [
"approx", "approx",
"astrokit", "astrokit",
"bytemuck", "bytemuck",
"criterion",
"gltf", "gltf",
"hifitime", "hifitime",
"image", "image",
@@ -1362,6 +1604,15 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -1420,6 +1671,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "simba" name = "simba"
version = "0.10.2" version = "0.10.2"
@@ -1597,6 +1854,16 @@ dependencies = [
"syn 3.0.9", "syn 3.0.9",
] ]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.53.3" version = "1.53.3"
@@ -1687,6 +1954,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.129" version = "0.2.129"
@@ -1958,6 +2235,22 @@ dependencies = [
"safe_arch", "safe_arch",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -1967,6 +2260,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.62.2" version = "0.62.2"
+20
View File
@@ -109,6 +109,7 @@ fn render(
}, },
appearance: file.appearance(), appearance: file.appearance(),
read_radiance: false, read_radiance: false,
profile: timings.is_some(),
}; };
let start = Instant::now(); let start = Instant::now();
@@ -121,6 +122,7 @@ fn render(
let loading = start.elapsed(); let loading = start.elapsed();
let mut frame_times = Vec::new(); let mut frame_times = Vec::new();
let mut pass_times = Vec::new();
let mut frame = None; let mut frame = None;
for _ in 0..timings.unwrap_or(1).max(1) { for _ in 0..timings.unwrap_or(1).max(1) {
let start = Instant::now(); let start = Instant::now();
@@ -129,6 +131,7 @@ fn render(
.and_then(|p| p.wait()) .and_then(|p| p.wait())
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
frame_times.push(start.elapsed().as_secs_f64() * 1e3); frame_times.push(start.elapsed().as_secs_f64() * 1e3);
pass_times.extend(rendered.timings);
frame = Some(rendered); frame = Some(rendered);
} }
let frame = frame.expect("at least one frame"); let frame = frame.expect("at least one frame");
@@ -153,6 +156,23 @@ fn render(
warm.len() warm.len()
); );
} }
// GPU time per pass, median over the frames after the first.
if pass_times.len() > 1 {
let median = |f: fn(&resolved::gpu::profile::PassTimings) -> f64| {
let mut v: Vec<f64> = pass_times[1..].iter().map(f).collect();
v.sort_by(f64::total_cmp);
v[v.len() / 2]
};
println!(
"gpu bodies {:.3} ms, mesh {:.3} ms, sensor {:.3} ms, first to last {:.3} ms (medians)",
median(|t| t.bodies),
median(|t| t.mesh),
median(|t| t.sensor),
median(|t| t.total)
);
} else if pass_times.is_empty() {
println!("gpu no pass timings: this adapter has no timestamp queries");
}
} }
let detector = &renderer.camera().detector; let detector = &renderer.camera().detector;
if let Some(out) = out { if let Some(out) = out {
+5
View File
@@ -35,3 +35,8 @@ stl_io = "0.11"
[dev-dependencies] [dev-dependencies]
approx = "0.5" approx = "0.5"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
criterion = "0.8"
[[bench]]
name = "render"
harness = false
+128
View File
@@ -0,0 +1,128 @@
//! End-to-end frame times: `submit` then `wait`, including reading the DN image back.
//!
//! Every frame shows a model in the example scenes' composition: an isometric view
//! from low orbit with Earth's limb 2 degrees above the image center. Models are the
//! box-wing from the repository and, if `scripts/fetch-models.sh` has been run,
//! Landsat 8 (85k triangles) and ICESat-2 (752k triangles). Each runs on the GPU and
//! on lavapipe at 1024 x 1024 and 1920 x 1080.
//!
//! Per-pass GPU times are not here; `resolved render --timings N` reports them.
use std::path::{Path, PathBuf};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use resolved::gpu::AdapterChoice;
use resolved::nalgebra::{Matrix3, Rotation3, UnitQuaternion, Vector3};
use resolved::radiometry::AU;
use resolved::{CameraConfig, LoadOptions, Mesh, Pose, Renderer, RendererOptions, Scene, Target};
const EARTH_RADIUS: f64 = 6_378_137.0;
const ORBIT: f64 = 7.0e6;
fn examples() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/models")
}
/// The models to bench, skipping NASA models that have not been fetched.
fn models() -> Vec<(&'static str, Mesh)> {
let mut models = vec![(
"boxwing",
Mesh::load(&examples().join("boxwing.gltf"), &LoadOptions::default()).unwrap(),
)];
for name in ["landsat8", "icesat2"] {
let path = examples().join("nasa").join(format!("{name}.glb"));
if path.is_file() {
models.push((name, Mesh::load(&path, &LoadOptions::default()).unwrap()));
} else {
eprintln!("skipping {name}: run scripts/fetch-models.sh to bench it");
}
}
models
}
/// The scene and observer for `mesh`, as in `scripts/make-example-scenes.py`.
fn limb_view(mesh: &Mesh, model: resolved::ModelId) -> (Scene, Pose) {
let (mut lo, mut hi) = (Vector3::repeat(f64::MAX), Vector3::repeat(f64::MIN));
for p in &mesh.positions {
let p = Vector3::from(*p).cast::<f64>();
lo = lo.inf(&p);
hi = hi.sup(&p);
}
let center = (lo + hi) / 2.0;
let radius = (hi - lo).norm() / 2.0;
let limb = (EARTH_RADIUS / ORBIT).asin();
let theta = limb - 2f64.to_radians();
let look = Vector3::new(ORBIT, 0.0, 0.0);
let boresight = Vector3::new(-theta.cos(), theta.sin(), 0.0);
// Body (1, 1, 1) toward the camera, body +Z toward the local vertical.
let frame = |a: Vector3<f64>, b: Vector3<f64>| {
let t1 = a.normalize();
let t2 = a.cross(&b).normalize();
Matrix3::from_columns(&[t1, t2, t1.cross(&t2)])
};
let rotation =
frame(-boresight, Vector3::x()) * frame(Vector3::repeat(1.0), Vector3::z()).transpose();
let attitude = UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix(&rotation));
let distance = radius / 2.8f64.to_radians().tan();
let observer = Pose::looking_at(look - boresight * distance, look, Vector3::x()).unwrap();
let scene = Scene {
earth_attitude: UnitQuaternion::identity(),
sun: Vector3::new(0.743, -0.669, 0.017).normalize() * AU,
moon: Vector3::new(0.0, 0.0, -3.844e8),
targets: vec![Target {
model,
pose: Pose::new(look - attitude * center, attitude),
}],
};
(scene, observer)
}
fn camera(width: u32, height: u32) -> CameraConfig {
let mut camera = CameraConfig::example();
camera.width = width;
camera.height = height;
camera
}
fn frames(c: &mut Criterion) {
let models = models();
for (device, adapter) in [("gpu", AdapterChoice::Gpu), ("cpu", AdapterChoice::Cpu)] {
let mut group = c.benchmark_group(format!("frame/{device}"));
if device == "cpu" {
group.sample_size(20);
}
for (width, height) in [(1024, 1024), (1920, 1080)] {
let options = RendererOptions {
adapter: adapter.clone(),
..Default::default()
};
let mut renderer = Renderer::new(camera(width, height), options).unwrap();
for (name, mesh) in &models {
let model = renderer.load_model(mesh).unwrap();
let (scene, observer) = limb_view(mesh, model);
let id = BenchmarkId::new(format!("{width}x{height}"), name);
group.bench_function(id, |b| {
b.iter(|| renderer.submit(&scene, &observer).unwrap().wait().unwrap())
});
}
}
group.finish();
}
}
fn setup(c: &mut Criterion) {
let mut group = c.benchmark_group("setup");
group.sample_size(10);
// Create and drop as one cycle. Timing creation alone depends on when criterion
// drops the previous renderer, and with the driver unloading in between it came out
// bimodal (16 or 160 ms) before the Vulkan instance was shared.
group.bench_function("renderer/gpu/1920x1080", |b| {
b.iter(|| drop(Renderer::new(camera(1920, 1080), RendererOptions::default()).unwrap()))
});
group.finish();
}
criterion_group!(benches, frames, setup);
criterion_main!(benches);
+5 -1
View File
@@ -6,6 +6,7 @@
use nalgebra::Vector3; use nalgebra::Vector3;
use super::profile::PassTimestamps;
use super::targets::{FrameTargets, DEPTH_COMPARE, DEPTH_FORMAT, RADIANCE_FORMAT}; use super::targets::{FrameTargets, DEPTH_COMPARE, DEPTH_FORMAT, RADIANCE_FORMAT};
use super::GpuContext; use super::GpuContext;
use crate::bodies::{Appearance, EARTH_EQUATORIAL_RADIUS, EARTH_POLAR_RADIUS, MOON_RADIUS}; use crate::bodies::{Appearance, EARTH_EQUATORIAL_RADIUS, EARTH_POLAR_RADIUS, MOON_RADIUS};
@@ -186,13 +187,15 @@ impl BodiesPass {
} }
} }
/// Uploads this frame's uniforms and records the pass into `encoder`. /// Uploads this frame's uniforms and records the pass into `encoder`, writing
/// `timestamps` around it if given.
pub fn encode( pub fn encode(
&self, &self,
ctx: &GpuContext, ctx: &GpuContext,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
targets: &FrameTargets, targets: &FrameTargets,
uniforms: &BodiesUniforms, uniforms: &BodiesUniforms,
timestamps: Option<PassTimestamps>,
) { ) {
ctx.queue ctx.queue
.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); .write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
@@ -217,6 +220,7 @@ impl BodiesPass {
}), }),
stencil_ops: None, stencil_ops: None,
}), }),
timestamp_writes: timestamps.map(PassTimestamps::render),
..Default::default() ..Default::default()
}); });
pass.set_pipeline(&self.pipeline); pass.set_pipeline(&self.pipeline);
+15 -8
View File
@@ -41,25 +41,32 @@ impl std::fmt::Debug for GpuContext {
/// Every adapter this machine offers, in the order the drivers report them. /// Every adapter this machine offers, in the order the drivers report them.
pub fn adapters() -> Vec<wgpu::AdapterInfo> { pub fn adapters() -> Vec<wgpu::AdapterInfo> {
let instance = instance(); pollster::block_on(instance().enumerate_adapters(BACKENDS))
pollster::block_on(instance.enumerate_adapters(BACKENDS))
.iter() .iter()
.map(|a| a.get_info()) .map(|a| a.get_info())
.collect() .collect()
} }
fn instance() -> wgpu::Instance { /// The one wgpu instance this process uses, created on first use and never dropped.
wgpu::Instance::new(wgpu::InstanceDescriptor { ///
backends: BACKENDS, /// One per renderer would be simpler, but creating and destroying Vulkan instances on
..wgpu::InstanceDescriptor::new_without_display_handle_from_env() /// several threads at once crashes the Vulkan loader (1.4.363 here): when the last
/// instance goes, it unloads the drivers, and an instance being created on another
/// thread at that moment calls into a driver that is no longer loaded.
fn instance() -> &'static wgpu::Instance {
static INSTANCE: std::sync::OnceLock<wgpu::Instance> = std::sync::OnceLock::new();
INSTANCE.get_or_init(|| {
wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: BACKENDS,
..wgpu::InstanceDescriptor::new_without_display_handle_from_env()
})
}) })
} }
impl GpuContext { impl GpuContext {
/// Opens a device on the adapter `choice` selects. /// Opens a device on the adapter `choice` selects.
pub fn new(choice: &AdapterChoice) -> Result<Self, Error> { pub fn new(choice: &AdapterChoice) -> Result<Self, Error> {
let instance = instance(); let candidates = pollster::block_on(instance().enumerate_adapters(BACKENDS));
let candidates = pollster::block_on(instance.enumerate_adapters(BACKENDS));
let infos: Vec<_> = candidates.iter().map(|a| a.get_info()).collect(); let infos: Vec<_> = candidates.iter().map(|a| a.get_info()).collect();
let kinds: Vec<_> = infos let kinds: Vec<_> = infos
.iter() .iter()
+3
View File
@@ -7,6 +7,7 @@
use nalgebra::{Matrix3, Vector3}; use nalgebra::{Matrix3, Vector3};
use super::bodies::MAX_CHANNELS; use super::bodies::MAX_CHANNELS;
use super::profile::PassTimestamps;
use super::targets::{FrameTargets, DEPTH_COMPARE, DEPTH_FORMAT, RADIANCE_FORMAT}; use super::targets::{FrameTargets, DEPTH_COMPARE, DEPTH_FORMAT, RADIANCE_FORMAT};
use super::GpuContext; use super::GpuContext;
use crate::camera::Intrinsics; use crate::camera::Intrinsics;
@@ -284,6 +285,7 @@ impl MeshPass {
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
targets: &FrameTargets, targets: &FrameTargets,
draws: &[(&GpuMesh, MeshDraw)], draws: &[(&GpuMesh, MeshDraw)],
timestamps: Option<PassTimestamps>,
) { ) {
if draws.len() > self.capacity { if draws.len() > self.capacity {
self.capacity = draws.len().next_power_of_two(); self.capacity = draws.len().next_power_of_two();
@@ -318,6 +320,7 @@ impl MeshPass {
}), }),
stencil_ops: None, stencil_ops: None,
}), }),
timestamp_writes: timestamps.map(PassTimestamps::render),
..Default::default() ..Default::default()
}); });
pass.set_pipeline(&self.pipeline); pass.set_pipeline(&self.pipeline);
+1
View File
@@ -3,6 +3,7 @@
pub mod bodies; pub mod bodies;
pub mod context; pub mod context;
pub mod mesh; pub mod mesh;
pub mod profile;
pub mod sensor; pub mod sensor;
pub mod targets; pub mod targets;
+127
View File
@@ -0,0 +1,127 @@
//! GPU timestamps around each pass, for seeing where a frame's time goes.
//!
//! Each pass writes a timestamp when it starts and when it ends. After the frame the
//! queries are resolved into a buffer and read back with the image. Needs the
//! adapter's `TIMESTAMP_QUERY` feature; without it there is nothing to measure and
//! [`Profiler::new`] returns `None`.
use super::GpuContext;
/// The passes timed, in the order they run.
pub const PASSES: [&str; 3] = ["bodies", "mesh", "sensor"];
/// Where one pass writes its begin and end timestamps.
#[derive(Clone, Copy, Debug)]
pub struct PassTimestamps<'a> {
set: &'a wgpu::QuerySet,
begin: u32,
}
impl<'a> PassTimestamps<'a> {
/// For a render pass descriptor.
pub fn render(self) -> wgpu::RenderPassTimestampWrites<'a> {
wgpu::RenderPassTimestampWrites {
query_set: self.set,
beginning_of_pass_write_index: Some(self.begin),
end_of_pass_write_index: Some(self.begin + 1),
}
}
/// For a compute pass descriptor.
pub fn compute(self) -> wgpu::ComputePassTimestampWrites<'a> {
wgpu::ComputePassTimestampWrites {
query_set: self.set,
beginning_of_pass_write_index: Some(self.begin),
end_of_pass_write_index: Some(self.begin + 1),
}
}
}
/// GPU time spent in each pass of one frame, in milliseconds.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PassTimings {
/// Earth, Moon and Sun.
pub bodies: f64,
/// Target meshes.
pub mesh: f64,
/// Radiance to DN.
pub sensor: f64,
/// From the start of the first pass to the end of the last, gaps included.
pub total: f64,
}
/// A timestamp query set with the buffers to read it back.
#[derive(Debug)]
pub struct Profiler {
set: wgpu::QuerySet,
resolve: wgpu::Buffer,
/// Read back with the frame; the renderer maps it alongside the image.
pub staging: wgpu::Buffer,
/// Nanoseconds per timestamp tick.
period: f64,
}
const QUERIES: u32 = 2 * PASSES.len() as u32;
impl Profiler {
/// A profiler, if the device was opened with timestamp queries.
pub fn new(ctx: &GpuContext) -> Option<Self> {
if !ctx
.device
.features()
.contains(wgpu::Features::TIMESTAMP_QUERY)
{
return None;
}
let size = QUERIES as u64 * 8;
let buffer = |label, usage| {
ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
})
};
Some(Self {
set: ctx.device.create_query_set(&wgpu::QuerySetDescriptor {
label: Some("pass timestamps"),
ty: wgpu::QueryType::Timestamp,
count: QUERIES,
}),
resolve: buffer(
"timestamp resolve",
wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
),
staging: buffer(
"timestamp staging",
wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
),
period: ctx.queue.get_timestamp_period() as f64,
})
}
/// The timestamps for pass `index` of [`PASSES`].
pub fn pass(&self, index: usize) -> PassTimestamps<'_> {
PassTimestamps {
set: &self.set,
begin: 2 * index as u32,
}
}
/// Records resolving the queries into the staging buffer, after the last pass.
pub fn resolve(&self, encoder: &mut wgpu::CommandEncoder) {
encoder.resolve_query_set(&self.set, 0..QUERIES, &self.resolve, 0);
encoder.copy_buffer_to_buffer(&self.resolve, 0, &self.staging, 0, self.resolve.size());
}
/// Converts mapped staging contents to milliseconds.
pub fn timings(&self, ticks: &[u64]) -> PassTimings {
let ms = |a: u64, b: u64| b.wrapping_sub(a) as f64 * self.period * 1e-6;
PassTimings {
bodies: ms(ticks[0], ticks[1]),
mesh: ms(ticks[2], ticks[3]),
sensor: ms(ticks[4], ticks[5]),
total: ms(ticks[0], ticks[5]),
}
}
}
+3 -1
View File
@@ -4,6 +4,7 @@
//! writes [`FrameTargets::dn`]. No noise yet (M3); the result for each pixel is //! writes [`FrameTargets::dn`]. No noise yet (M3); the result for each pixel is
//! [`Detector::dn`](crate::camera::Detector::dn) of [`Radiometry::electrons`]. //! [`Detector::dn`](crate::camera::Detector::dn) of [`Radiometry::electrons`].
use super::profile::PassTimestamps;
use super::targets::FrameTargets; use super::targets::FrameTargets;
use super::GpuContext; use super::GpuContext;
use crate::camera::CameraConfig; use crate::camera::CameraConfig;
@@ -88,6 +89,7 @@ impl SensorPass {
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
targets: &FrameTargets, targets: &FrameTargets,
uniforms: &SensorUniforms, uniforms: &SensorUniforms,
timestamps: Option<PassTimestamps>,
) { ) {
ctx.queue ctx.queue
.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); .write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
@@ -115,7 +117,7 @@ impl SensorPass {
let (x, y) = (groups.min(65_535), groups.div_ceil(65_535)); let (x, y) = (groups.min(65_535), groups.div_ceil(65_535));
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("sensor"), label: Some("sensor"),
timestamp_writes: None, timestamp_writes: timestamps.map(PassTimestamps::compute),
}); });
pass.set_pipeline(&self.pipeline); pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bind_group, &[]); pass.set_bind_group(0, &bind_group, &[]);
+59 -11
View File
@@ -15,6 +15,7 @@ use crate::camera::CameraConfig;
use crate::eclipse::sun_visible_fraction; use crate::eclipse::sun_visible_fraction;
use crate::gpu::bodies::{BodiesPass, BodiesUniforms, BodyRadiometry}; use crate::gpu::bodies::{BodiesPass, BodiesUniforms, BodyRadiometry};
use crate::gpu::mesh::{GpuMesh, MeshDraw, MeshPass}; use crate::gpu::mesh::{GpuMesh, MeshDraw, MeshPass};
use crate::gpu::profile::{PassTimings, Profiler};
use crate::gpu::sensor::{SensorPass, SensorUniforms}; use crate::gpu::sensor::{SensorPass, SensorUniforms};
use crate::gpu::targets::FrameTargets; use crate::gpu::targets::FrameTargets;
use crate::gpu::{AdapterChoice, GpuContext}; use crate::gpu::{AdapterChoice, GpuContext};
@@ -33,6 +34,9 @@ pub struct RendererOptions {
/// Also read back the channel radiance image each frame. Costs another 16 bytes per /// Also read back the channel radiance image each frame. Costs another 16 bytes per
/// pixel of transfer; for debugging and tests. /// pixel of transfer; for debugging and tests.
pub read_radiance: bool, pub read_radiance: bool,
/// Time each pass on the GPU and report it in [`Frame::timings`]. Needs an adapter
/// with timestamp queries; without one, timings are `None`.
pub profile: bool,
} }
/// One rendered image. /// One rendered image.
@@ -48,6 +52,9 @@ pub struct Frame {
pub dn: Vec<u16>, pub dn: Vec<u16>,
/// Channel radiance, if [`RendererOptions::read_radiance`] was set. /// Channel radiance, if [`RendererOptions::read_radiance`] was set.
pub radiance: Option<Vec<[f32; 4]>>, pub radiance: Option<Vec<[f32; 4]>>,
/// GPU time per pass, if [`RendererOptions::profile`] was set and the adapter
/// supports timestamps.
pub timings: Option<PassTimings>,
} }
impl Frame { impl Frame {
@@ -58,6 +65,11 @@ impl Frame {
} }
/// Renders frames for one camera. /// Renders frames for one camera.
///
/// Create and drop renderers from one thread at a time. With current drivers
/// (NVIDIA 615.71 and Mesa's lavapipe, measured on mainframe), creating or dropping
/// Vulkan devices on several threads at once occasionally crashes or hangs inside the
/// driver. Rendering on several renderers at once, once they exist, is not affected.
#[derive(Debug)] #[derive(Debug)]
pub struct Renderer { pub struct Renderer {
ctx: GpuContext, ctx: GpuContext,
@@ -72,6 +84,7 @@ pub struct Renderer {
models: Vec<GpuMesh>, models: Vec<GpuMesh>,
dn_staging: wgpu::Buffer, dn_staging: wgpu::Buffer,
radiance_staging: Option<(wgpu::Buffer, u32)>, radiance_staging: Option<(wgpu::Buffer, u32)>,
profiler: Option<Profiler>,
} }
impl Renderer { impl Renderer {
@@ -104,6 +117,11 @@ impl Renderer {
row, row,
) )
}); });
let profiler = if options.profile {
Profiler::new(&ctx)
} else {
None
};
Ok(Self { Ok(Self {
bodies: BodiesPass::new(&ctx), bodies: BodiesPass::new(&ctx),
meshes: MeshPass::new(&ctx), meshes: MeshPass::new(&ctx),
@@ -117,6 +135,7 @@ impl Renderer {
models: Vec::new(), models: Vec::new(),
dn_staging, dn_staging,
radiance_staging, radiance_staging,
profiler,
}) })
} }
@@ -176,11 +195,21 @@ impl Renderer {
let ctx = &self.ctx; let ctx = &self.ctx;
let mut encoder = ctx.device.create_command_encoder(&Default::default()); let mut encoder = ctx.device.create_command_encoder(&Default::default());
let stamps = |i| self.profiler.as_ref().map(|p| p.pass(i));
self.bodies self.bodies
.encode(ctx, &mut encoder, &self.targets, &bodies); .encode(ctx, &mut encoder, &self.targets, &bodies, stamps(0));
self.meshes.encode(ctx, &mut encoder, &self.targets, &draws); self.meshes
self.sensor .encode(ctx, &mut encoder, &self.targets, &draws, stamps(1));
.encode(ctx, &mut encoder, &self.targets, &self.sensor_uniforms); self.sensor.encode(
ctx,
&mut encoder,
&self.targets,
&self.sensor_uniforms,
stamps(2),
);
if let Some(profiler) = &self.profiler {
profiler.resolve(&mut encoder);
}
encoder.copy_buffer_to_buffer( encoder.copy_buffer_to_buffer(
&self.targets.dn, &self.targets.dn,
0, 0,
@@ -211,9 +240,7 @@ impl Renderer {
// Ask for the staging buffers now; the mapping completes when the GPU finishes. // Ask for the staging buffers now; the mapping completes when the GPU finishes.
let (sender, mapped) = mpsc::channel(); let (sender, mapped) = mpsc::channel();
let staged = for buffer in self.staging_buffers() {
std::iter::once(&self.dn_staging).chain(self.radiance_staging.as_ref().map(|(b, _)| b));
for buffer in staged {
let sender = sender.clone(); let sender = sender.clone();
buffer buffer
.slice(..) .slice(..)
@@ -225,6 +252,13 @@ impl Renderer {
mapped, mapped,
}) })
} }
/// Every buffer read back after a frame.
fn staging_buffers(&self) -> impl Iterator<Item = &wgpu::Buffer> {
std::iter::once(&self.dn_staging)
.chain(self.radiance_staging.as_ref().map(|(b, _)| b))
.chain(self.profiler.as_ref().map(|p| &p.staging))
}
} }
/// A frame the GPU is working on. [`Pending::wait`] returns it. /// A frame the GPU is working on. [`Pending::wait`] returns it.
@@ -271,12 +305,28 @@ impl Pending<'_> {
Some(texels) Some(texels)
} }
}; };
let timings = match &r.profiler {
None => None,
Some(profiler) => {
let timings = {
let view = profiler
.staging
.slice(..)
.get_mapped_range()
.map_err(gpu_error)?;
profiler.timings(bytemuck::cast_slice::<u8, u64>(&view))
};
profiler.staging.unmap();
Some(timings)
}
};
Ok(Frame { Ok(Frame {
width: r.camera.width, width: r.camera.width,
height: r.camera.height, height: r.camera.height,
channels: r.camera.channels.len() as u32, channels: r.camera.channels.len() as u32,
dn, dn,
radiance, radiance,
timings,
}) })
} }
@@ -293,8 +343,7 @@ impl Pending<'_> {
timeout: None, timeout: None,
}) })
.map_err(gpu_error)?; .map_err(gpu_error)?;
let buffers = 1 + usize::from(r.radiance_staging.is_some()); for _ in r.staging_buffers() {
for _ in 0..buffers {
self.mapped.recv().map_err(gpu_error)?.map_err(gpu_error)?; self.mapped.recv().map_err(gpu_error)?.map_err(gpu_error)?;
} }
Ok(()) Ok(())
@@ -304,8 +353,7 @@ impl Pending<'_> {
impl Drop for Pending<'_> { impl Drop for Pending<'_> {
fn drop(&mut self) { fn drop(&mut self) {
if self.index.is_some() && self.block().is_ok() { if self.index.is_some() && self.block().is_ok() {
self.renderer.dn_staging.unmap(); for buffer in self.renderer.staging_buffers() {
if let Some((buffer, _)) = &self.renderer.radiance_staging {
buffer.unmap(); buffer.unmap();
} }
} }
+1 -1
View File
@@ -60,7 +60,7 @@ fn render_with(
&body_radiometry(), &body_radiometry(),
); );
let mut encoder = ctx.device.create_command_encoder(&Default::default()); let mut encoder = ctx.device.create_command_encoder(&Default::default());
pass.encode(ctx, &mut encoder, &targets, &uniforms); pass.encode(ctx, &mut encoder, &targets, &uniforms, None);
ctx.queue.submit([encoder.finish()]); ctx.queue.submit([encoder.finish()]);
let radiance = targets.read_radiance(ctx); let radiance = targets.read_radiance(ctx);
common::save(name, &ctx.info, &radiance); common::save(name, &ctx.info, &radiance);
+10
View File
@@ -15,6 +15,16 @@ use resolved::radiometry::{Radiometry, AU};
use resolved::CameraConfig; use resolved::CameraConfig;
use wgpu::AdapterInfo; use wgpu::AdapterInfo;
/// 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
/// the drivers on mainframe (NVIDIA 615.71, lavapipe): measured 4 crashes and 2 hangs
/// in 150 runs of the renderer tests in parallel, none in 100 run one at a time.
pub fn device_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// A hardware GPU and lavapipe, opened once per test binary and shared by its tests. /// A hardware GPU and lavapipe, opened once per test binary and shared by its tests.
pub fn contexts() -> &'static [GpuContext; 2] { pub fn contexts() -> &'static [GpuContext; 2] {
static CONTEXTS: OnceLock<[GpuContext; 2]> = OnceLock::new(); static CONTEXTS: OnceLock<[GpuContext; 2]> = OnceLock::new();
+3
View File
@@ -3,6 +3,8 @@
//! Needs a Vulkan GPU and Mesa's lavapipe (`vulkan-swrast` on Arch, //! Needs a Vulkan GPU and Mesa's lavapipe (`vulkan-swrast` on Arch,
//! `mesa-vulkan-drivers` on Debian and Ubuntu). //! `mesa-vulkan-drivers` on Debian and Ubuntu).
mod common;
use resolved::gpu::{AdapterChoice, GpuContext}; use resolved::gpu::{AdapterChoice, GpuContext};
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
@@ -72,6 +74,7 @@ fn double_plus_one(ctx: &GpuContext, input: &[f32]) -> Vec<f32> {
} }
fn check(choice: AdapterChoice, expect_cpu: bool) { fn check(choice: AdapterChoice, expect_cpu: bool) {
let _devices = common::device_lock();
let ctx = GpuContext::new(&choice).unwrap(); let ctx = GpuContext::new(&choice).unwrap();
assert_eq!(ctx.is_cpu(), expect_cpu, "{choice:?} picked {:?}", ctx.info); assert_eq!(ctx.is_cpu(), expect_cpu, "{choice:?} picked {:?}", ctx.info);
let input: Vec<f32> = (0..1000).map(|i| i as f32 * 0.5).collect(); let input: Vec<f32> = (0..1000).map(|i| i as f32 * 0.5).collect();
+2 -2
View File
@@ -117,8 +117,8 @@ fn render(ctx: &GpuContext, name: &str, scene: &Scene, camera: &Pose, mesh: &Mes
}) })
.collect(); .collect();
let mut encoder = ctx.device.create_command_encoder(&Default::default()); let mut encoder = ctx.device.create_command_encoder(&Default::default());
bodies.encode(ctx, &mut encoder, &targets, &uniforms); bodies.encode(ctx, &mut encoder, &targets, &uniforms, None);
meshes.encode(ctx, &mut encoder, &targets, &draws); meshes.encode(ctx, &mut encoder, &targets, &draws, None);
ctx.queue.submit([encoder.finish()]); ctx.queue.submit([encoder.finish()]);
let radiance = targets.read_radiance(ctx); let radiance = targets.read_radiance(ctx);
common::save(name, &ctx.info, &radiance); common::save(name, &ctx.info, &radiance);
+39
View File
@@ -80,6 +80,7 @@ fn cube_target(model: ModelId) -> Target {
#[test] #[test]
fn sunlit_cube_matches_the_cpu_chain() { fn sunlit_cube_matches_the_cpu_chain() {
let _devices = common::device_lock();
let config = CameraConfig::example(); let config = CameraConfig::example();
let r = Radiometry::new(&config); let r = Radiometry::new(&config);
let ray = config.intrinsics().ray(&Vector2::new(960.5, 540.5)); let ray = config.intrinsics().ray(&Vector2::new(960.5, 540.5));
@@ -108,6 +109,7 @@ fn sunlit_cube_matches_the_cpu_chain() {
#[test] #[test]
fn camera_mount_is_rigid() { fn camera_mount_is_rigid() {
let _devices = common::device_lock();
// Mounting the camera rotated on the observer, and turning the observer the other // Mounting the camera rotated on the observer, and turning the observer the other
// way, must give the same image as no mount at all. // way, must give the same image as no mount at all.
let turn = UnitQuaternion::from_euler_angles(0.3, -0.2, 1.1); let turn = UnitQuaternion::from_euler_angles(0.3, -0.2, 1.1);
@@ -155,6 +157,7 @@ fn camera_mount_is_rigid() {
#[test] #[test]
fn frames_are_independent() { fn frames_are_independent() {
let _devices = common::device_lock();
for mut renderer in renderers(false) { for mut renderer in renderers(false) {
let model = renderer.load_model(&cube()).unwrap(); let model = renderer.load_model(&cube()).unwrap();
let with = scene(vec![cube_target(model)]); let with = scene(vec![cube_target(model)]);
@@ -185,8 +188,44 @@ fn frames_are_independent() {
#[test] #[test]
fn unknown_model_is_an_error() { fn unknown_model_is_an_error() {
let _devices = common::device_lock();
let mut renderer = Renderer::new(CameraConfig::example(), Default::default()).unwrap(); let mut renderer = Renderer::new(CameraConfig::example(), Default::default()).unwrap();
assert!(renderer assert!(renderer
.submit(&scene(vec![cube_target(ModelId(0))]), &observer()) .submit(&scene(vec![cube_target(ModelId(0))]), &observer())
.is_err()); .is_err());
} }
#[test]
fn profiling_reports_pass_times() {
let _devices = common::device_lock();
for profile in [false, true] {
for adapter in [AdapterChoice::Gpu, AdapterChoice::Cpu] {
let options = RendererOptions {
adapter,
profile,
..Default::default()
};
let mut renderer = Renderer::new(CameraConfig::example(), options).unwrap();
let model = renderer.load_model(&cube()).unwrap();
let frame = renderer
.submit(&scene(vec![cube_target(model)]), &observer())
.unwrap()
.wait()
.unwrap();
let name = &renderer.adapter_info().name;
match (profile, frame.timings) {
(false, timings) => assert!(timings.is_none(), "{name}"),
(true, None) => {
panic!("{name}: no timings; both mainframe adapters support timestamps")
}
(true, Some(t)) => {
for pass in [t.bodies, t.mesh, t.sensor] {
assert!(pass > 0.0 && pass <= t.total, "{name}: {t:?}");
}
// A frame takes under a second even on lavapipe.
assert!(t.total < 1000.0, "{name}: {t:?}");
}
}
}
}
}
+3 -3
View File
@@ -35,7 +35,7 @@ fn sense(ctx: &GpuContext, name: &str, radiance: &[[f32; 4]]) -> Vec<u16> {
let pass = SensorPass::new(ctx); let pass = SensorPass::new(ctx);
let uniforms = SensorUniforms::new(&config, &Radiometry::new(&config)); let uniforms = SensorUniforms::new(&config, &Radiometry::new(&config));
let mut encoder = ctx.device.create_command_encoder(&Default::default()); let mut encoder = ctx.device.create_command_encoder(&Default::default());
pass.encode(ctx, &mut encoder, &targets, &uniforms); pass.encode(ctx, &mut encoder, &targets, &uniforms, None);
ctx.queue.submit([encoder.finish()]); ctx.queue.submit([encoder.finish()]);
let dn = targets.read_dn(ctx); let dn = targets.read_dn(ctx);
common::save_dn(name, &ctx.info, &dn); common::save_dn(name, &ctx.info, &dn);
@@ -160,8 +160,8 @@ fn sunlit_earth_through_the_whole_chain() {
for ctx in common::contexts() { for ctx in common::contexts() {
let targets = FrameTargets::new(ctx, config.width, config.height, 3); let targets = FrameTargets::new(ctx, config.width, config.height, 3);
let mut encoder = ctx.device.create_command_encoder(&Default::default()); let mut encoder = ctx.device.create_command_encoder(&Default::default());
BodiesPass::new(ctx).encode(ctx, &mut encoder, &targets, &bodies); BodiesPass::new(ctx).encode(ctx, &mut encoder, &targets, &bodies, None);
SensorPass::new(ctx).encode(ctx, &mut encoder, &targets, &sensor); SensorPass::new(ctx).encode(ctx, &mut encoder, &targets, &sensor, None);
ctx.queue.submit([encoder.finish()]); ctx.queue.submit([encoder.finish()]);
let dn = targets.read_dn(ctx); let dn = targets.read_dn(ctx);
common::save_dn("sensor-earth", &ctx.info, &dn); common::save_dn("sensor-earth", &ctx.info, &dn);
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+35
View File
@@ -18,6 +18,30 @@ Rendering goes through wgpu. On a machine without a GPU, the same shaders run
on the CPU through Mesa's lavapipe (`vulkan-swrast` on Arch, on the CPU through Mesa's lavapipe (`vulkan-swrast` on Arch,
`mesa-vulkan-drivers` on Debian and Ubuntu). `mesa-vulkan-drivers` on Debian and Ubuntu).
## Performance and fidelity
Lighting and camera fidelity are chosen separately: lighting tiers L0 to L3,
camera tiers C0 to C3. A row is added here as each tier lands, with its
measured cost and a sample image from the same scene, so the cost of each step
up is visible at a glance.
| Tier | Lighting | Camera | RTX 3060 | lavapipe (CPU) | Sample |
|---|---|---|---|---|---|
| L0 / C0 | Lambertian surfaces, Earth's and the Moon's limbs exact, eclipse by Earth and the Moon, no shadows on the target, no light from Earth | Pinhole, absolute radiometry to 12-bit DN, no noise or blur | 1.8–2.1 ms | 8–65 ms | [![L0/C0](docs/images/l0-c0.png)](docs/images/l0-c0.png) |
Times are one 1920 x 1080 RGB frame end to end (`submit` to `wait`, readback
of the DN image included), median of criterion runs, over models from 12 to
752,000 triangles: the box-wing, Landsat 8 and ICESat-2. The GPU's own work is
0.3 to 0.6 ms of that; reading 12 MB back and copying it out is most of the
rest. lavapipe ran on the Intel Core Ultra 9 285K's 24 cores. The run-to-run
noise is about 3 %. The sample is the Landsat 8 example scene
(`examples/scenes/landsat8.toml`). `cargo bench -p resolved` reproduces the
numbers, and `resolved render --timings N` shows the per-pass split.
Planned tiers: L1 adds self-shadowing, Earthshine and albedo maps (M2); C1
and C2 add lens distortion, blur and detector noise (M3); L2 adds measured
materials and the atmosphere (M4); L3 is path traced (M5).
## Usage ## Usage
```rust ```rust
@@ -149,6 +173,17 @@ them needs a Vulkan driver for the GPU and lavapipe installed. Set
`WGPU_BACKEND`, `WGPU_ADAPTER_NAME` and the other standard wgpu environment `WGPU_BACKEND`, `WGPU_ADAPTER_NAME` and the other standard wgpu environment
variables to steer adapter discovery. variables to steer adapter discovery.
Benchmarks are end-to-end frame times (`submit` then `wait`, readback
included) on the GPU and on lavapipe, at two resolutions, for the box-wing and,
once fetched, Landsat 8 and ICESat-2:
```sh
cargo bench -p resolved
```
For where a frame's time goes, `resolved render --timings N` adds GPU
timestamps around each pass (`RendererOptions::profile` in the library).
To see what the GPU tests render, set `RESOLVED_TEST_IMAGES=1`. Each test then To see what the GPU tests render, set `RESOLVED_TEST_IMAGES=1`. Each test then
writes a PNG per device to `target/tmp/test-images/`, scaled so a white writes a PNG per device to `target/tmp/test-images/`, scaled so a white
surface facing the Sun is full white: surface facing the Sun is full white: