mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 22:17:53 -06:00
Merge PR #3370
This commit is contained in:
+30
-2
@@ -386,7 +386,8 @@ fn module_source(m: &Module, consts: &str) -> String {
|
||||
}
|
||||
|
||||
impl Gpu {
|
||||
/// Create a device on the best available adapter (no software fallback), or why there is none.
|
||||
/// Create a device on the best available adapter (no software adapter unless
|
||||
/// `LIGHTCRAFT_GPU_ALLOW_SOFTWARE`), or why there is none.
|
||||
pub fn new(backends: wgpu::Backends) -> Result<Gpu, String> {
|
||||
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
|
||||
// only these drivers are loaded (`crate::backend`: never Vulkan on Windows unless asked)
|
||||
@@ -401,10 +402,13 @@ impl Gpu {
|
||||
)
|
||||
.map_err(|e| format!("no GPU adapter among {backends:?} ({e})"))?;
|
||||
let info = adapter.get_info();
|
||||
if info.device_type == wgpu::DeviceType::Cpu {
|
||||
if refuse_adapter(info.device_type, software_allowed()) {
|
||||
log::info!("gpu: only a software adapter ({}), using the CPU pipeline", info.name);
|
||||
return Err(format!("software adapter ({}) skipped: the CPU pipeline is faster", info.name));
|
||||
}
|
||||
if info.device_type == wgpu::DeviceType::Cpu {
|
||||
log::info!("gpu: LIGHTCRAFT_GPU_ALLOW_SOFTWARE: using the software adapter {}", info.name);
|
||||
}
|
||||
let mut limits = adapter.limits();
|
||||
if limits.max_storage_buffers_per_shader_stage < 10 {
|
||||
log::info!("gpu: {} has too few storage buffers per stage", info.name);
|
||||
@@ -747,6 +751,19 @@ impl Gpu {
|
||||
/// normally reset a hung GPU much sooner).
|
||||
const READBACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
/// Whether to turn down an adapter of `device_type`: a software rasterizer (llvmpipe / lavapipe,
|
||||
/// WARP) is slower than the CPU pipeline, so it is refused unless `allow_software`.
|
||||
fn refuse_adapter(device_type: wgpu::DeviceType, allow_software: bool) -> bool {
|
||||
device_type == wgpu::DeviceType::Cpu && !allow_software
|
||||
}
|
||||
|
||||
/// `LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1`: accept a software adapter. For tests on machines without a
|
||||
/// GPU (Mesa lavapipe on Linux, WARP on Windows), so the CPU ↔ GPU equivalence tests run there
|
||||
/// instead of skipping; the app never sets it (software rendering is slower than the CPU pipeline).
|
||||
fn software_allowed() -> bool {
|
||||
std::env::var("LIGHTCRAFT_GPU_ALLOW_SOFTWARE").is_ok_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
||||
}
|
||||
|
||||
/// `LIGHTCRAFT_GPU_LIMITS`: lower the device limits, e.g. to reproduce a smaller adapter.
|
||||
/// `webgpu` / `downlevel` = 128 MiB storage bindings and 256 MiB buffers (the WebGPU defaults),
|
||||
/// `<n>` = n MiB storage bindings and 2n MiB buffers. Returns (binding bytes, buffer bytes).
|
||||
@@ -1153,6 +1170,17 @@ mod tests {
|
||||
println!("CHILD: survived");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn software_adapters_need_the_opt_in() {
|
||||
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
|
||||
assert!(super::refuse_adapter(Cpu, false), "llvmpipe / WARP are refused by default");
|
||||
assert!(!super::refuse_adapter(Cpu, true), "LIGHTCRAFT_GPU_ALLOW_SOFTWARE accepts them");
|
||||
for t in [DiscreteGpu, IntegratedGpu, VirtualGpu, Other] {
|
||||
assert!(!super::refuse_adapter(t, false), "{t:?}");
|
||||
assert!(!super::refuse_adapter(t, true), "{t:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatches_stay_within_the_workgroup_limit() {
|
||||
// 1-D kernels over a 100 MP image and 2-D ones over 16k × 16k stay under 65535 per axis
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! `lightcraft_gpu::render`, compared in 8-bit sRGB.
|
||||
//!
|
||||
//! Bounds (documented in `docs/gpu-pipeline.md`): mean |Δ| < 0.5 LSB and max |Δ| ≤ 3 LSB per
|
||||
//! channel for every case. Skips (passes with a note) when no GPU adapter exists, e.g. on CI.
|
||||
//! channel for every case. Skips (passes with a note) when no GPU adapter exists;
|
||||
//! `LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1` runs them on a software adapter (lavapipe, WARP) instead.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -889,13 +890,20 @@ fn windows_match() {
|
||||
check("window: vignette corner", &src, &info, &s, &corner);
|
||||
}
|
||||
|
||||
/// Issue #323: a window of a 24 MP photo is cut from a source of 288 MB on the device. Keeping that
|
||||
/// upload with the view's stages counted it against the stage budget on every tick of a drag and
|
||||
/// got the stages cleared, and the upload and resampling redone: the window's stages are its own
|
||||
/// pixels, not the photo's.
|
||||
/// Issue #323: a window of a big photo is cut from the whole source on the device (288 MB for 24 MP).
|
||||
/// Keeping that upload with the view's stages counted it against the stage budget on every tick of
|
||||
/// a drag and got the stages cleared, and the upload and resampling redone: the window's stages are
|
||||
/// its own pixels, not the photo's.
|
||||
// (these two share the process's one big source: a test lock keeps them from replacing each other's)
|
||||
static BIG_SOURCE_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// The big photo of these tests: 10.24 MP, a 117 MiB source. That is over what a view keeps with its
|
||||
/// stages (96 MiB, so it is shared) and within a 128 MiB storage binding (the WebGPU default, and
|
||||
/// the limit of Mesa's lavapipe), so the source goes to the device on every adapter. A source
|
||||
/// over the device's limit is resampled on the CPU instead and never uploaded, which would leave
|
||||
/// these tests nothing to check.
|
||||
const BIG: usize = 3200;
|
||||
|
||||
#[test]
|
||||
fn a_window_does_not_keep_the_whole_source_on_the_device() {
|
||||
let _lock = BIG_SOURCE_TESTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -903,11 +911,11 @@ fn a_window_does_not_keep_the_whole_source_on_the_device() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_pipeline::PixelWindow;
|
||||
let src = scene(1, 6000, 4000);
|
||||
let src = scene(1, BIG, BIG);
|
||||
let info = SourceInfo { raw: true, ..Default::default() };
|
||||
let stages = StageCache::default();
|
||||
let s = DevelopSettings::default();
|
||||
let req = RenderRequest { window: Some(PixelWindow { x: 2000, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(6000, 4000) };
|
||||
let req = RenderRequest { window: Some(PixelWindow { x: 2000, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(BIG, BIG) };
|
||||
let _ = lightcraft_gpu::render(&src, &info, &s, &req, Some(&stages)).expect("gpu render");
|
||||
let held = lightcraft_gpu::stage_bytes(&stages);
|
||||
assert!(held < 100 << 20, "the view's GPU stages hold {} MiB for a 640×480 window", held >> 20);
|
||||
@@ -919,7 +927,7 @@ fn a_window_does_not_keep_the_whole_source_on_the_device() {
|
||||
assert!(lightcraft_gpu::stage_bytes(&stages) < 100 << 20);
|
||||
}
|
||||
|
||||
/// …but the 288 MB is uploaded once for every window of every view of the photo, not once per
|
||||
/// …but the source is uploaded once for every window of every view of the photo, not once per
|
||||
/// window: panning at 1:1, and the Before and After windows, share one device copy of it, which
|
||||
/// goes when the app idles (`trim_pool(0)`).
|
||||
#[test]
|
||||
@@ -929,17 +937,17 @@ fn windows_of_a_big_photo_share_one_uploaded_source() {
|
||||
return;
|
||||
}
|
||||
use lightcraft_pipeline::PixelWindow;
|
||||
let src = scene(1, 6000, 4000);
|
||||
let src = scene(1, BIG, BIG);
|
||||
let info = SourceInfo { raw: true, ..Default::default() };
|
||||
let s = DevelopSettings::default();
|
||||
let (after, before) = (StageCache::default(), StageCache::default());
|
||||
let at = |x: usize| RenderRequest { window: Some(PixelWindow { x, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(6000, 4000) };
|
||||
let at = |x: usize| RenderRequest { window: Some(PixelWindow { x, y: 1500, w: 640, h: 480 }), ..RenderRequest::fit(BIG, BIG) };
|
||||
let start = lightcraft_gpu::source_uploads();
|
||||
for (cache, x) in [(&after, 1000), (&after, 1256), (&after, 1512), (&before, 1512), (&before, 1768)] {
|
||||
lightcraft_gpu::render(&src, &info, &s, &at(x), Some(cache)).expect("gpu render");
|
||||
}
|
||||
assert_eq!(lightcraft_gpu::source_uploads() - start, 1, "one upload for five windows of two views");
|
||||
assert!(lightcraft_gpu::shared_source_bytes() >= 280_000_000, "{}", lightcraft_gpu::shared_source_bytes());
|
||||
assert!(lightcraft_gpu::shared_source_bytes() >= BIG * BIG * 12, "{}", lightcraft_gpu::shared_source_bytes());
|
||||
assert!(lightcraft_gpu::stage_bytes(&after) < 100 << 20, "and it is not in the views' stages");
|
||||
lightcraft_gpu::trim_pool(0);
|
||||
assert_eq!(lightcraft_gpu::shared_source_bytes(), 0, "an idle app gives it back");
|
||||
|
||||
@@ -49,6 +49,9 @@ Overrides (read once at launch):
|
||||
- `LIGHTCRAFT_GPU=0`: GPU rendering off for the process (the window is unaffected).
|
||||
- Both `LIGHTCRAFT_GPU=0` and `LIGHTCRAFT_GPU_BACKEND=off` also keep AI Denoise, the face models
|
||||
and the AI mask model (SAM 3 on Metal) on the CPU.
|
||||
- `LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1`: accept a software adapter (llvmpipe / lavapipe, WARP), which is
|
||||
otherwise refused because the CPU pipeline is faster. For running the GPU tests on a machine
|
||||
without a GPU (below); the app never sets it.
|
||||
|
||||
**DX12 shader compiler (issue #471).** Every instance (window and compute) compiles DX12 shaders with
|
||||
FXC (`d3dcompiler_47.dll`, part of Windows; `lightcraft_gpu::backend::backend_options`). wgpu's
|
||||
@@ -272,6 +275,32 @@ bit-identical, and one (the point colour range overlay) has max 2 LSB on 0.0002%
|
||||
mean 0.0003 LSB. That is the same picture as on Metal, inside the bounds
|
||||
(`cargo test -p lightcraft-gpu --release --test it equivalence:: -- --nocapture`).
|
||||
|
||||
**Running them without a GPU (issue #1050).** `Gpu::new` turns down software adapters, so on a
|
||||
machine without a hardware GPU (a VM, a container) every GPU test skips.
|
||||
`LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1` lets them through for one run (tests only: the app never sets it):
|
||||
|
||||
```sh
|
||||
log="$(mktemp -d)/gpu-skips.jsonl" # a new private directory; an absolute path, as tests run in crates/gpu
|
||||
LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1 LIGHTCRAFT_SKIP_LOG="$log" cargo test -p lightcraft-gpu --tests
|
||||
grep '"need":"gpu"' "$log" | grep -v -e 'lost on purpose' -e 'memory never ran out' # no output: only the two below skipped
|
||||
```
|
||||
|
||||
- **Linux:** Mesa's lavapipe (Vulkan; `mesa-vulkan-drivers` on Debian and Ubuntu). Its storage
|
||||
bindings stop at 128 MiB (the WebGPU default), so the two big-source tests use a 117 MiB source:
|
||||
above what a view keeps with its stages (96 MiB) and small enough to be uploaded there.
|
||||
- **Windows:** WARP, the DX12 software rasterizer that ships with Windows (shaders through FXC, as
|
||||
above).
|
||||
- **macOS:** Metal is a hardware adapter, so the tests run without the variable.
|
||||
|
||||
A machine with a hardware GPU picks it over the software adapter, variable or not. Each test that
|
||||
skips prints `skipped (gpu): …` and appends a line to the `LIGHTCRAFT_SKIP_LOG` file; with a software
|
||||
adapter the only ones left should be the device-loss test (`no GPU device, or one that can't be lost on
|
||||
purpose here`) and the device-memory test (`no GPU device, or its memory never ran out`: on lavapipe its
|
||||
allocations never fail, seen 2026-10-31). Measured 2026-10-11 on lavapipe and WARP, and again 2026-10-30 on lavapipe (Mesa's
|
||||
llvmpipe on LLVM 20, 143 compared renders), every compared render stayed within **max 1 LSB, mean ≤
|
||||
0.0003 LSB**, as on the M4 Pro. The equivalence binary takes about 20 s on lavapipe and several
|
||||
minutes on WARP.
|
||||
|
||||
Remaining differences come from f32 vs f64 coordinate math, fast-math transcendental functions on
|
||||
Metal and summation order in the box filters — all far below one 8-bit step.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user