mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 03:27:14 -06:00
Merge upstream/main into pr/overlay-windows (#1027 landed)
GPU handles(): only output-device proofs stay on the CPU; Show All Masks now renders zoom windows (upstream); proof-boundary tolerance in the GPU equivalence test; device-profile draft test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ const MAX_LSB: u8 = 3;
|
||||
fn gpu() -> bool {
|
||||
let ok = lightcraft_gpu::available();
|
||||
if !ok {
|
||||
eprintln!("skipped: no GPU adapter");
|
||||
lightcraft_testkit::skip!(Gpu, "no GPU adapter");
|
||||
}
|
||||
ok
|
||||
}
|
||||
@@ -79,6 +79,19 @@ fn check(name: &str, src: &Arc<Rgb32f>, info: &SourceInfo, s: &DevelopSettings,
|
||||
(mean, max)
|
||||
}
|
||||
|
||||
/// [`check`] for gamut-warning renders: a warning is a hard decision at the gamut boundary, so a
|
||||
/// pixel sitting exactly on it may be painted by one renderer and not the other (float rounding).
|
||||
/// Such flips are allowed for at most 0.01 % of the pixels; everything else is held to [`check`]'s bounds.
|
||||
fn check_warned(name: &str, src: &Arc<Rgb32f>, info: &SourceInfo, s: &DevelopSettings, req: &RenderRequest) {
|
||||
let cpu = render(src, info, s, req).image;
|
||||
let gpu = lightcraft_gpu::render(src, info, s, req, None).expect("gpu render");
|
||||
let (mean, _, over) = diff(&cpu, &gpu.image);
|
||||
let flips = cpu.data.iter().zip(&gpu.image.data).filter(|(p, q)| (0..3).any(|c| p[c].abs_diff(q[c]) > MAX_LSB)).count();
|
||||
let share = flips as f64 / cpu.data.len() as f64;
|
||||
eprintln!("{name:<28} {}x{} mean {mean:.4} >1: {:.4}% boundary flips {flips}", cpu.width, cpu.height, over * 100.0);
|
||||
assert!(mean < MEAN_LSB && share <= 1e-4, "{name}: mean {mean:.4} LSB, {flips} pixels differ by more than {MAX_LSB} LSB");
|
||||
}
|
||||
|
||||
type Edit = fn(&mut DevelopSettings);
|
||||
|
||||
fn typical(s: &mut DevelopSettings) {
|
||||
@@ -93,6 +106,26 @@ fn typical(s: &mut DevelopSettings) {
|
||||
s.detail.sharpen_amount = 40.0;
|
||||
}
|
||||
|
||||
/// Issue #960: a preview's noise reduction works in the original's pixels (a smaller radius, a
|
||||
/// smaller `eps`, faded below one pixel). The GPU takes the same parameters (`local::nr_params`).
|
||||
#[test]
|
||||
fn preview_noise_reduction_matches() {
|
||||
if !gpu() {
|
||||
return;
|
||||
}
|
||||
let src = scene(3, 640, 480);
|
||||
let mut s = DevelopSettings::default();
|
||||
s.detail.nr_luminance = 70.0;
|
||||
s.detail.nr_detail = 30.0;
|
||||
s.detail.nr_color = 60.0;
|
||||
s.detail.nr_color_smoothness = 50.0;
|
||||
// a preview level of a 6000-px original: loupe-, grid- and tiny-thumbnail-sized outputs
|
||||
let info = SourceInfo { raw: true, native_long: Some(6000), ..Default::default() };
|
||||
for (name, edge) in [("NR preview 640 of 6000", 640), ("NR preview 320 of 6000", 320), ("NR preview 128 of 6000", 128)] {
|
||||
check(name, &src, &info, &s, &RenderRequest::fit(edge, edge));
|
||||
}
|
||||
}
|
||||
|
||||
/// A render big enough for the per-pixel stage to run in several bands of rows (and the render
|
||||
/// in many submissions), with grain (position-dependent) and masks.
|
||||
#[test]
|
||||
@@ -876,7 +909,7 @@ fn soft_proofs_match() {
|
||||
saturated(&mut s);
|
||||
let proof = Some(Proof { space, dest_warning, display_warning, profile: 0 });
|
||||
let req = RenderRequest { space: out, proof, ..RenderRequest::fit(640, 640) };
|
||||
check(&format!("proof {space:?} → {out:?} warn {dest_warning}/{display_warning}"), &src, &raw, &s, &req);
|
||||
check_warned(&format!("proof {space:?} → {out:?} warn {dest_warning}/{display_warning}"), &src, &raw, &s, &req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ fn masked(s: &mut DevelopSettings, k: usize) {
|
||||
#[test]
|
||||
fn failed_gpu_renders_fall_back_with_a_reason() {
|
||||
if !lightcraft_gpu::available() {
|
||||
eprintln!("skipped: no GPU adapter ({:?})", lightcraft_gpu::unavailable_reason());
|
||||
lightcraft_testkit::skip!(Gpu, "no GPU adapter ({})", lightcraft_gpu::unavailable_reason().unwrap_or_default());
|
||||
return;
|
||||
}
|
||||
assert_eq!(lightcraft_gpu::unavailable_reason(), None);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! What GPU this machine offers the develop pipeline, as one JSON line: the capability probe of
|
||||
//! `cargo xtask ci`, which is std + serde_json and has no wgpu of its own. Ignored in normal runs;
|
||||
//! xtask runs it after the workspace tests, so it costs no extra build:
|
||||
//! `cargo test --workspace --test gpu_probe -- --ignored --nocapture`.
|
||||
//!
|
||||
//! - `LIGHTCRAFT_GPU_PROBE` unset: the compute device the pipeline picks (`available()`,
|
||||
//! `adapter_name()`, `unavailable_reason()`), the backends it may use and the backends this build
|
||||
//! contains.
|
||||
//! - `LIGHTCRAFT_GPU_PROBE=<backend>` (`vulkan`, `metal`, `dx12`, `gl`): every adapter that
|
||||
//! backend lists, software ones (lavapipe, llvmpipe, WARP) included. One backend per process, so
|
||||
//! a driver that crashes while it is enumerated (issue #136) costs only its own line.
|
||||
//!
|
||||
//! Only reports: which adapters the pipeline accepts is `ctx.rs`'s policy.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use lightcraft_gpu::backend::{self, Backends};
|
||||
|
||||
const NAMES: [(&str, Backends); 4] = [("vulkan", Backends::VULKAN), ("metal", Backends::METAL), ("dx12", Backends::DX12), ("gl", Backends::GL)];
|
||||
|
||||
#[test]
|
||||
#[ignore = "the capability probe of `cargo xtask ci`"]
|
||||
fn gpu_probe() {
|
||||
let line = match std::env::var("LIGHTCRAFT_GPU_PROBE").ok().filter(|b| !b.is_empty()) {
|
||||
None => pipeline(),
|
||||
Some(name) => adapters(&name),
|
||||
};
|
||||
// its own line, prefixed: libtest may have printed `test gpu_probe ... ` on this one
|
||||
println!("\ngpu_probe: {line}");
|
||||
}
|
||||
|
||||
fn names(set: Backends) -> String {
|
||||
let list: Vec<String> = NAMES.iter().filter(|(_, b)| set.contains(*b)).map(|(n, _)| json_str(n)).collect();
|
||||
format!("[{}]", list.join(","))
|
||||
}
|
||||
|
||||
fn pipeline() -> String {
|
||||
let available = lightcraft_gpu::available();
|
||||
let opt = |s: Option<String>| s.map_or_else(|| "null".to_owned(), |s| json_str(&s));
|
||||
let compiled = wgpu::Instance::enabled_backend_features();
|
||||
format!(
|
||||
"{{\"pipeline\":{{\"available\":{available},\"adapter\":{},\"reason\":{},\"backends\":{},\"compiled\":{}}}}}",
|
||||
opt(lightcraft_gpu::adapter_name()),
|
||||
opt(lightcraft_gpu::unavailable_reason()),
|
||||
names(backend::compute_backends().unwrap_or(Backends::empty()) & compiled),
|
||||
names(compiled),
|
||||
)
|
||||
}
|
||||
|
||||
fn adapters(name: &str) -> String {
|
||||
let (_, set) = NAMES.iter().find(|(n, _)| *n == name).unwrap_or_else(|| panic!("unknown backend `{name}` (vulkan, metal, dx12, gl)"));
|
||||
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
|
||||
desc.backends = *set;
|
||||
desc.backend_options = backend::backend_options();
|
||||
desc.flags = backend::instance_flags_from_env(desc.flags);
|
||||
let instance = wgpu::Instance::new(desc);
|
||||
let adapters: Vec<String> = pollster::block_on(instance.enumerate_adapters(*set))
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let i = a.get_info();
|
||||
format!(
|
||||
"{{\"name\":{},\"type\":{},\"vendor\":\"0x{:04x}\",\"device\":\"0x{:04x}\",\"driver\":{},\"driver_info\":{}}}",
|
||||
json_str(&i.name),
|
||||
json_str(&format!("{:?}", i.device_type)),
|
||||
i.vendor,
|
||||
i.device,
|
||||
json_str(&i.driver),
|
||||
json_str(&i.driver_info),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
format!("{{\"backend\":{},\"adapters\":[{}]}}", json_str(name), adapters.join(","))
|
||||
}
|
||||
|
||||
/// A JSON string literal.
|
||||
fn json_str(s: &str) -> String {
|
||||
let mut out = String::from("\"");
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
c if u32::from(c) < 0x20 => out.push_str(&format!("\\u{:04x}", u32::from(c))),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use lightcraft_pipeline::{RenderRequest, SourceInfo, StageCache};
|
||||
#[test]
|
||||
fn stages_are_counted_and_the_pool_is_bounded_and_trimmed() {
|
||||
if !lightcraft_gpu::available() {
|
||||
eprintln!("skipped: no GPU adapter");
|
||||
lightcraft_testkit::skip!(Gpu, "no GPU adapter");
|
||||
return;
|
||||
}
|
||||
let limit = 64u64 << 20;
|
||||
|
||||
@@ -21,7 +21,8 @@ fn nothing_renders_on_the_gpu_once_the_process_is_ending() {
|
||||
let req = RenderRequest::fit(w, h);
|
||||
let has_gpu = lightcraft_gpu::available();
|
||||
if !has_gpu {
|
||||
eprintln!("no GPU adapter ({:?}): only the CPU side is checked", lightcraft_gpu::unavailable_reason());
|
||||
let why = lightcraft_gpu::unavailable_reason().unwrap_or_default();
|
||||
lightcraft_testkit::skip!(Gpu, "no GPU adapter ({why}), so only the CPU side is checked");
|
||||
}
|
||||
assert!(!lightcraft_gpu::shutting_down());
|
||||
assert!(lightcraft_gpu::wait_idle(Duration::ZERO), "nothing is in flight yet");
|
||||
|
||||
Reference in New Issue
Block a user