From da62ea8c499d1fff2d38e9e0c6dd9eab8b97c05e Mon Sep 17 00:00:00 2001 From: hxw <530313@qq.com> Date: Fri, 9 Oct 2026 04:34:51 -0500 Subject: [PATCH] LR-BEHAV-GPU: compile DX12 shaders with FXC, never a stray dxcompiler.dll (issue #471) wgpu's default DX12 compiler (Auto) loads whichever dxcompiler.dll the DLL search path finds first. LightCraft ships none, so it picked up another program's copy; one without dxil.dll beside it (e.g. DXC 1.7) warns that the DXIL is unsigned, wgpu treats that as a compile error, its indirect-validation pipelines fail, the device is lost and the window never opens ("Parent device is lost", exit code 1). Every wgpu instance (window and both compute devices) now uses lightcraft_gpu::backend::backend_options(): wgpu's env options with the DX12 compiler pinned to FXC (d3dcompiler_47.dll, part of Windows), unless WGPU_DX12_COMPILER asks for another. Co-Authored-By: Claude Opus 5.5 --- apps/lightcraft/src/main.rs | 14 +++++++++++++- crates/gpu/src/backend.rs | 38 +++++++++++++++++++++++++++++++++++++ crates/gpu/src/ctx.rs | 2 ++ crates/gpu/src/nn.rs | 2 ++ docs/gpu-pipeline.md | 11 +++++++++++ docs/whats-new.md | 3 +++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index a0394da95..c278bc02d 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -76,11 +76,13 @@ impl eframe::App for App { /// The window's renderer (egui-wgpu): eframe's defaults, with LightCraft's backend choice — DX12 /// alone on Windows unless `LIGHTCRAFT_GPU_BACKEND` / `WGPU_BACKEND` say otherwise (issue #136: -/// with Vulkan in the set, wgpu loads the Vulkan driver even when it then picks DX12). +/// with Vulkan in the set, wgpu loads the Vulkan driver even when it then picks DX12) — and DX12 +/// shaders compiled with FXC, never a stray `dxcompiler.dll` (issue #471). fn window_wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration { let mut c = eframe::egui_wgpu::WgpuConfiguration::default(); if let eframe::egui_wgpu::WgpuSetup::CreateNew(n) = &mut c.wgpu_setup { n.instance_descriptor.backends = lightcraft_engine::gpu::backend::window_backends(); + n.instance_descriptor.backend_options = lightcraft_engine::gpu::backend::backend_options(); } c } @@ -898,6 +900,16 @@ mod tests { assert!(!b.is_empty()); } + /// Issue #471: the window's DX12 shaders compile with FXC — wgpu's default loaded any + /// `dxcompiler.dll` on the search path, and one without `dxil.dll` kept the window from opening. + #[test] + fn window_compiles_dx12_shaders_with_fxc() { + let eframe::egui_wgpu::WgpuSetup::CreateNew(n) = window_wgpu_options().wgpu_setup else { panic!("expected CreateNew") }; + if std::env::var_os("WGPU_DX12_COMPILER").is_none() { + assert!(matches!(n.instance_descriptor.backend_options.dx12.shader_compiler, eframe::wgpu::Dx12Compiler::Fxc)); + } + } + /// Issue #234: Windows opens links with the URL protocol handler (the default browser), not /// File Explorer, and passes the whole map link, `?`, `&` and `#` included, as one argument. #[test] diff --git a/crates/gpu/src/backend.rs b/crates/gpu/src/backend.rs index fcdea55fa..041f57737 100644 --- a/crates/gpu/src/backend.rs +++ b/crates/gpu/src/backend.rs @@ -121,6 +121,30 @@ pub fn window_backends() -> Backends { resolve(choice, default, wgpu::Instance::enabled_backend_features()).unwrap_or(default) } +/// The DX12 shader compiler: FXC (`d3dcompiler_47.dll`, part of Windows) unless +/// `WGPU_DX12_COMPILER` (`wgpu`, wgpu's own variable: `fxc`, `dxc`, `auto`…) names another. +/// +/// Issue #471: wgpu's default (`Auto`) loads whichever `dxcompiler.dll` the DLL search path finds +/// first — LightCraft ships none, so it is some other program's (an SDK's, a folder on `PATH`). A +/// copy without its `dxil.dll` beside it warns that the DXIL is unsigned; wgpu takes the warning for +/// a compile error, its own validation pipelines fail, the device is lost and the window never +/// opens. FXC is always there and compiles everything LightCraft's shaders need. +pub fn dx12_compiler(env: Option<&str>) -> wgpu::Dx12Compiler { + let Some(v) = env else { return wgpu::Dx12Compiler::Fxc }; + v.parse().unwrap_or_else(|e| { + log::warn!("gpu: WGPU_DX12_COMPILER={v} ignored: {e}"); + wgpu::Dx12Compiler::Fxc + }) +} + +/// wgpu's backend options for every instance LightCraft creates — the window's and the compute +/// devices': wgpu's environment variables, with the DX12 compiler from [`dx12_compiler`]. +pub fn backend_options() -> wgpu::BackendOptions { + let mut o = wgpu::BackendOptions::from_env_or_default(); + o.dx12.shader_compiler = dx12_compiler(std::env::var("WGPU_DX12_COMPILER").ok().as_deref()); + o +} + /// `LIGHTCRAFT_GPU_BACKEND=off`. pub(crate) fn env_off() -> bool { env_choice() == BackendChoice::Off @@ -211,6 +235,20 @@ mod tests { assert!(default_window_backends("linux").contains(Backends::VULKAN | Backends::GL)); } + /// Issue #471: DX12 shaders compile with FXC, never a `dxcompiler.dll` found on the search path, + /// unless `WGPU_DX12_COMPILER` asks for it. + #[test] + fn dx12_compiles_with_fxc_unless_asked() { + assert!(matches!(dx12_compiler(None), wgpu::Dx12Compiler::Fxc)); + assert!(matches!(dx12_compiler(Some("bogus")), wgpu::Dx12Compiler::Fxc)); + assert!(matches!(dx12_compiler(Some("FXC")), wgpu::Dx12Compiler::Fxc)); + assert!(matches!(dx12_compiler(Some("dxc")), wgpu::Dx12Compiler::DynamicDxc { .. })); + assert!(matches!(dx12_compiler(Some("auto")), wgpu::Dx12Compiler::Auto)); + if std::env::var_os("WGPU_DX12_COMPILER").is_none() { + assert!(matches!(backend_options().dx12.shader_compiler, wgpu::Dx12Compiler::Fxc)); + } + } + #[test] fn resolves_against_the_build() { let compiled = Backends::DX12 | Backends::VULKAN; diff --git a/crates/gpu/src/ctx.rs b/crates/gpu/src/ctx.rs index 0f826bf44..2f4abd4d2 100644 --- a/crates/gpu/src/ctx.rs +++ b/crates/gpu/src/ctx.rs @@ -282,6 +282,8 @@ impl Gpu { let mut desc = wgpu::InstanceDescriptor::new_without_display_handle(); // only these drivers are loaded (`crate::backend`: never Vulkan on Windows unless asked) desc.backends = backends; + // DX12 shaders compile with FXC (issue #471) + desc.backend_options = crate::backend::backend_options(); let instance = wgpu::Instance::new(desc); let adapter = pollster::block_on( instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, ..Default::default() }), diff --git a/crates/gpu/src/nn.rs b/crates/gpu/src/nn.rs index 4231dae8c..bb9d5eea7 100644 --- a/crates/gpu/src/nn.rs +++ b/crates/gpu/src/nn.rs @@ -76,6 +76,8 @@ fn create_device() -> Result { fn make_device(backends: wgpu::Backends) -> Result { let mut desc = wgpu::InstanceDescriptor::new_without_display_handle(); desc.backends = backends; + // DX12 shaders compile with FXC (issue #471) + desc.backend_options = crate::backend::backend_options(); let instance = wgpu::Instance::new(desc); let wanted = std::env::var("LIGHTCRAFT_GPU_ADAPTER").ok().map(|s| s.trim().to_lowercase()).filter(|s| !s.is_empty()); let adapter = match &wanted { diff --git a/docs/gpu-pipeline.md b/docs/gpu-pipeline.md index 5eb0ce1d2..c843d5a06 100644 --- a/docs/gpu-pipeline.md +++ b/docs/gpu-pipeline.md @@ -44,6 +44,17 @@ Overrides (read once at launch): the compute device always used Vulkan + DX12 (+ Metal). - `LIGHTCRAFT_GPU=0`: GPU rendering off for the process (the window is unaffected). +**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 +default, `Auto`, loads whichever `dxcompiler.dll` the DLL search path finds first. LightCraft ships +none, so that copy belongs to another program (a Windows SDK, a folder on `PATH`). An older copy without +`dxil.dll` beside it (e.g. DXC 1.7) warns that the DXIL is unsigned, wgpu treats the warning as a +compile error, its indirect-validation pipelines fail, the device is lost, and the window never opened +(`WGPU error: Parent device is lost`). `WGPU_DX12_COMPILER=dxc | auto | fxc` (wgpu's variable) still +chooses; with `dxc`, put `dxcompiler.dll` *and* `dxil.dll` (DXC ≥ 1.8.2502) next to `lightcraft.exe`. +FXC builds the compute kernels somewhat slower than DXC (≈ 9 s vs 7.5 s for the GPU test suite on an +RTX 4080), off the startup path. + **Off the startup path.** The compute device is created on a background thread once the window is up (`gpu::warm_up` from the first frame's settings), and never while GPU rendering is off: with Settings ▸ Performance ▸ *Use the GPU for rendering* unchecked (applied before the window opens), diff --git a/docs/whats-new.md b/docs/whats-new.md index ccb0be806..4767b59b1 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -149,6 +149,9 @@ ignored before) chooses the graphics backend; `off` renders on the CPU. The GPU now starts after the window is up, and only when Settings ▸ Performance ▸ Use the GPU for rendering is on; if LightCraft ever dies while starting the GPU, the next launch starts with GPU rendering off and says how to turn it back on. +- The Windows app no longer quits at launch with "Parent device is lost" when another program's `dxcompiler.dll` is + on the DLL search path without its `dxil.dll` (issue #471): DirectX 12 shaders now always compile with the + compiler built into Windows (FXC). `WGPU_DX12_COMPILER=dxc` uses a `dxcompiler.dll` instead. - Exports and renders never write over a photo's original (issue #93): exporting into the photo's own folder with the same name and "Overwrite" (or Export with Previous repeating it), an exact output path from the control channel or MCP, a merge preview path or `lightcraft-cli render IMG.jpg -o IMG.jpg` is refused with a clear