From 0bd3f3715df2433786c5b9d8c350ea21be50d916 Mon Sep 17 00:00:00 2001 From: radialmonster Date: Sat, 10 Oct 2026 12:11:41 -0400 Subject: [PATCH] LR-BEHAV-GPU: no indirect-call validation pipeline, which Intel Metal fails to compile (issue #250) On a 2015 MacBook Pro (Intel Iris/Metal), 0.5.0 never opened its window, even with LIGHTCRAFT_GPU=0: wgpu's indirect-call validation builds an internal compute pipeline when the window's device is created, Metal's compiler fails on it ("Compiler encountered an internal error"), the device is lost and eframe exits. That validation only guards indirect draws and dispatches, which neither LightCraft nor egui issue. backend::instance_flags clears InstanceFlags::VALIDATION_INDIRECT_CALL for every instance LightCraft creates (the window's, Gpu::new and the denoise device), unless WGPU_VALIDATION_INDIRECT_CALL asks for it. Tests: the flag rule, and that the window's instance descriptor goes without it. --- apps/lightcraft/src/main.rs | 12 ++++++++++++ crates/gpu/src/backend.rs | 30 ++++++++++++++++++++++++++++++ crates/gpu/src/ctx.rs | 2 ++ crates/gpu/src/nn.rs | 2 ++ 4 files changed, 46 insertions(+) diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index 00665e87..94aeefa1 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -98,6 +98,8 @@ fn window_wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration { 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(); + // no indirect-call validation pipeline: Intel Metal fails to compile it and the window never opens (#250) + n.instance_descriptor.flags = lightcraft_engine::gpu::backend::instance_flags_from_env(n.instance_descriptor.flags); } c } @@ -999,6 +1001,16 @@ mod tests { } } + /// Issue #250: the window's device is created without wgpu's indirect-call validation pipeline, + /// which Intel Macs' Metal compiler fails to build (the device was lost, the window never opened). + #[test] + fn window_has_no_indirect_call_validation_pipeline() { + let eframe::egui_wgpu::WgpuSetup::CreateNew(n) = window_wgpu_options().wgpu_setup else { panic!("expected CreateNew") }; + if std::env::var_os("WGPU_VALIDATION_INDIRECT_CALL").is_none() { + assert!(!n.instance_descriptor.flags.contains(eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL)); + } + } + /// 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 041f5773..81bfe719 100644 --- a/crates/gpu/src/backend.rs +++ b/crates/gpu/src/backend.rs @@ -145,6 +145,25 @@ pub fn backend_options() -> wgpu::BackendOptions { o } +/// wgpu's instance flags for every instance LightCraft creates, from `flags` (wgpu's defaults with its +/// environment variables applied) without `VALIDATION_INDIRECT_CALL`, unless +/// `WGPU_VALIDATION_INDIRECT_CALL` asks for it (any value but `0`). That validation builds an internal +/// compute pipeline when each device is created; Intel Macs' Metal compiler fails on it ("Compiler +/// encountered an internal error"), the device is lost and the window never opens (#250). It only +/// guards indirect draws and dispatches, which neither LightCraft nor egui issue. +pub fn instance_flags(flags: wgpu::InstanceFlags, env: Option<&str>) -> wgpu::InstanceFlags { + if env.is_some_and(|v| v != "0") { + flags | wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL + } else { + flags - wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL + } +} + +/// [`instance_flags`] with `WGPU_VALIDATION_INDIRECT_CALL` read from the environment. +pub fn instance_flags_from_env(flags: wgpu::InstanceFlags) -> wgpu::InstanceFlags { + instance_flags(flags, std::env::var("WGPU_VALIDATION_INDIRECT_CALL").ok().as_deref()) +} + /// `LIGHTCRAFT_GPU_BACKEND=off`. pub(crate) fn env_off() -> bool { env_choice() == BackendChoice::Off @@ -195,6 +214,17 @@ pub fn read_init_marker(path: &std::path::Path) -> Option { #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { + + /// #250: the indirect-call validation pipeline is off unless asked for; other flags are kept. + #[test] + fn indirect_call_validation_is_off_unless_asked_for() { + use wgpu::InstanceFlags as F; + let base = F::VALIDATION_INDIRECT_CALL | F::DEBUG; + assert_eq!(instance_flags(base, None), F::DEBUG); + assert_eq!(instance_flags(base, Some("0")), F::DEBUG); + assert_eq!(instance_flags(F::empty(), Some("1")), F::VALIDATION_INDIRECT_CALL); + assert_eq!(instance_flags(F::empty(), None), F::empty()); + } use super::*; #[test] diff --git a/crates/gpu/src/ctx.rs b/crates/gpu/src/ctx.rs index 2f4abd4d..8d73703c 100644 --- a/crates/gpu/src/ctx.rs +++ b/crates/gpu/src/ctx.rs @@ -284,6 +284,8 @@ impl Gpu { desc.backends = backends; // DX12 shaders compile with FXC (issue #471) desc.backend_options = crate::backend::backend_options(); + // no indirect-call validation pipeline: Intel Metal fails to compile it (#250) + desc.flags = crate::backend::instance_flags_from_env(desc.flags); 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 689aecbe..ca0975e9 100644 --- a/crates/gpu/src/nn.rs +++ b/crates/gpu/src/nn.rs @@ -86,6 +86,8 @@ fn make_device(backends: wgpu::Backends) -> Result { desc.backends = backends; // DX12 shaders compile with FXC (issue #471) desc.backend_options = crate::backend::backend_options(); + // no indirect-call validation pipeline: Intel Metal fails to compile it (#250) + desc.flags = crate::backend::instance_flags_from_env(desc.flags); 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 {