GPU: Classic 3D builds with FXC, so the GPU compositor starts on DirectX 12 (#613)

occlusion computes its three channels as one vector instead of writing a
vector component through a loop index, which FXC cannot address and tried
to unroll every enclosing loop for (X3550/X3511). classic3d now also
builds like the effect kernels: if a compiler rejects it, 3D runs render
on the CPU instead of the whole compositor failing to start. The ignored
FXC diagnostic test now checks classic3d instead of the stale pointwise
failure.
This commit is contained in:
Matej Kozlovský
2026-10-28 17:08:46 +01:00
parent fa9cf47175
commit 6bcb76b9dc
5 changed files with 117 additions and 27 deletions
+4
View File
@@ -126,6 +126,10 @@ const LIGHT_BYTES: usize = 6 * 16;
/// Composite a prepared Classic 3D run over `canvas`. `None` when it cannot run here (the
/// caller draws it on the CPU).
pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<GpuImage> {
// FXC (DirectX 12) may fail to build the kernel (#613): the caller renders the run on the CPU.
if !e.g.has_kernel("classic3d") {
return None;
}
let (w, h) = (canvas.width, canvas.height);
let boxes: Vec<[i64; 4]> = run.planes.iter().filter(|p| p.draw).map(|p| p.bbox).collect();
if boxes.is_empty() {
+62 -9
View File
@@ -30,7 +30,8 @@ const ENTRIES: &[&str] = &[
const EFFECT_ENTRIES: &[&str] = &["box_h", "box_v", "directional", "glow_bright", "glow_combine", "shadow_make", "shadow_combine", "pointwise"];
/// Entry points that also bind group 1 (four read-only storage buffers; see
/// [`Enc::dispatch_ext`]).
/// [`Enc::dispatch_ext`]). Like the effect kernels, one that fails to build (FXC on DirectX 12
/// can't unroll `classic3d`'s loops, #613) leaves its work to the CPU.
const EXT_ENTRIES: &[&str] = &["classic3d"];
/// Pixel format of every working texture: premultiplied RGBA, 32-bit float (32 bpc headroom;
@@ -616,15 +617,21 @@ impl GpuContext {
})
};
// The compositor's own kernels are required; an effect kernel that doesn't build is
// left out, and its effects render on the CPU (see `Enc::take_missing`).
let mut pipelines = ENTRIES
.iter()
.chain(crate::adv3d::SKY_KERNELS)
.map(|e| (e, &layout))
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
.map(|(e, layout)| Ok((*e, build(e, layout)?)))
.collect::<Result<HashMap<_, _>, String>>()?;
// left out, and its effects render on the CPU (see `Enc::take_missing`). So is a
// missing Classic 3D kernel: its 3D runs render on the CPU.
let mut pipelines = ENTRIES.iter().chain(crate::adv3d::SKY_KERNELS).map(|e| Ok((*e, build(e, &layout)?))).collect::<Result<HashMap<_, _>, String>>()?;
let mut unavailable = vec![];
for e in EXT_ENTRIES {
match build(e, &layout_ext) {
Ok(p) => {
pipelines.insert(*e, p);
}
Err(why) => {
log::warn!("gpu {name}: kernel {e} unavailable, its 3D layers render on the CPU: {why}");
unavailable.push((*e, why));
}
}
}
for e in EFFECT_ENTRIES
.iter()
.chain(crate::fx_color::KERNELS)
@@ -907,6 +914,11 @@ impl GpuContext {
&self.unavailable
}
/// Whether kernel `entry` was built (an effect or Classic 3D kernel can be missing, #613).
pub(crate) fn has_kernel(&self, entry: &str) -> bool {
self.pipelines.contains_key(entry)
}
/// Leave kernel `entry` out, as if it had failed to build (tests of the CPU fallback).
#[cfg(test)]
pub(crate) fn drop_kernel(&mut self, entry: &'static str) {
@@ -914,6 +926,47 @@ impl GpuContext {
self.unavailable.push((entry, "dropped by a test".into()));
}
/// A copy of this context sharing the same [`wgpu::Device`] and [`wgpu::Queue`] with
/// kernel `entry` left out, as if it had failed to build (tests of the CPU fallback
/// without opening a second device on `llvmpipe`).
#[cfg(test)]
pub(crate) fn without_kernel(&self, entry: &'static str) -> GpuContext {
let mut pipelines = self.pipelines.clone();
pipelines.remove(entry);
let mut unavailable = self.unavailable.clone();
unavailable.push((entry, "dropped by a test".into()));
GpuContext {
device: self.device.clone(),
queue: self.queue.clone(),
name: self.name.clone(),
max_dim: self.max_dim,
median_kernel: self.median_kernel,
bgl: self.bgl.clone(),
bgl_ext: self.bgl_ext.clone(),
pipelines,
unavailable,
display_bgl: self.display_bgl.clone(),
display: self.display.clone(),
display_mip: self.display_mip.clone(),
dummy_tex: self.dummy_tex.clone(),
dummy: self.dummy.clone(),
dummy_buf: self.dummy_buf.clone(),
uploads: Mutex::new(Uploads::default()),
upload_budget: std::sync::atomic::AtomicUsize::new(UPLOAD_BUDGET),
upload_full_warned: Default::default(),
pool: self.pool.clone(),
staging: Default::default(),
transfers: Default::default(),
zeros: Default::default(),
adv3d: std::sync::OnceLock::new(),
adv3d_raster: self.adv3d_raster,
particles: Default::default(),
particle_states: Default::default(),
deferred: self.deferred.clone(),
readbacks: self.readbacks.clone(),
}
}
/// Bytes of readback staging buffers kept for reuse (tests).
#[cfg(test)]
pub(crate) fn staging_bytes(&self) -> u64 {
+7 -9
View File
@@ -255,16 +255,14 @@ fn occlusion(c: u32, p: vec3<f32>, l: u32) -> vec3<f32> {
}
let tr = pl.mat1.y;
let dark = li.shadow.x;
var o = vec3<f32>(1.0);
for (var k = 0; k < 3; k++) {
var st = 0.0;
if (px.w > 0.0) {
st = clamp(px[k] / px.w, 0.0, 1.0);
}
let through = (1.0 - alpha) + alpha * tr * st;
o[k] = 1.0 - dark * (1.0 - through);
// Per channel, as whole vectors: FXC (DirectX 12) can't write a vector component through a
// loop index without unrolling every loop around the call, and fails (#613).
var st = vec3<f32>(0.0);
if (px.w > 0.0) {
st = clamp(px.xyz / px.w, vec3<f32>(0.0), vec3<f32>(1.0));
}
return o;
let through = (1.0 - alpha) + alpha * tr * st;
return 1.0 - dark * (1.0 - through);
}
fn shadow_of(it: u32, l: u32, p: vec3<f32>) -> vec3<f32> {
+12 -7
View File
@@ -148,23 +148,28 @@ fn opengl_adapters_get_cpu_compositing_at_once() {
assert!(t.elapsed() < std::time::Duration::from_secs(1), "{:?}", t.elapsed());
}
/// #613: FXC (DirectX 12's default shader compiler) builds Classic 3D, so the compositor starts
/// on DirectX 12 with its 3D kernel, and initialization leaves the caller's error scope and the
/// host's error handler untouched. Ignored by default: FXC compiles every kernel, which takes
/// minutes. Skips where there is no DirectX 12 adapter.
#[test]
#[cfg(target_os = "windows")]
#[ignore = "requires the reproduced Windows DX12/FXC pointwise compiler failure; no adapter skips"]
fn reproduced_fxc_pointwise_failure_returns_error_without_escaping_or_notifying_host() {
#[ignore = "slow: FXC compiles every kernel"]
fn fxc_builds_classic3d_and_initialization_errors_stay_scoped() {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = wgpu::Backends::DX12;
desc.backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::Fxc;
let (adapter, device, queue) = native_device(desc, true).expect("required DX12 device");
let Some((adapter, device, queue)) = native_device(desc, false) else { return };
assert_eq!(adapter.get_info().backend, wgpu::Backend::Dx12);
let host = count_host_errors(&device);
let outer = device.push_error_scope(wgpu::ErrorFilter::Validation);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| GpuContext::new(&adapter, device.clone(), queue)));
let error = match result.expect("FXC compiler failure must not escape the constructor") {
Err(error) => error,
Ok(_) => panic!("the platform-specific FXC failure no longer reproduces; reassess this diagnostic"),
let ctx = match result.expect("an FXC compiler failure must not escape the constructor") {
Ok(ctx) => ctx,
Err(error) => panic!("the compositor starts on DirectX 12 with FXC: {}", brief(error)),
};
assert!(error.contains("GPU initialization (pointwise)") && error.contains("FXC D3DCompile"), "{}", brief(error));
let missing: Vec<&str> = ctx.unavailable_kernels().iter().map(|(e, _)| *e).collect();
assert!(ctx.has_kernel("classic3d"), "FXC builds classic3d (unavailable: {missing:?})");
assert!(pollster::block_on(outer.pop()).is_none(), "constructor must drain its own scopes and leave the caller's scope clean");
assert_eq!(host.load(Ordering::SeqCst), 0);
valid_pipeline(&device);
+32 -2
View File
@@ -11,7 +11,7 @@ use effectcraft_project::{BitDepth, Comp, ItemKind, Layer, LayerSource, LightKin
use effectcraft_render::{Backend, RenderOpts, Renderer};
use effectcraft_time::{FrameRate, Tick};
use crate::tests::{Pattern, Scene, check, compare_at, gpu, n, opts, set};
use crate::tests::{Pattern, Scene, check, compare_at, diff, gpu, n, opts, set, tolerance};
const W: u32 = 160;
const H: u32 = 100;
@@ -209,6 +209,10 @@ fn the_gpu_draws_classic_3d_runs_itself() {
assert_eq!(prep.planes.iter().filter(|p| p.draw).count(), 3);
let mut e = crate::context::Enc::new(g.context());
let canvas = e.image(W, H);
if !g.context().has_kernel("classic3d") {
// FXC (DirectX 12) can't build the kernel (#613); the CPU fallback is tested below.
return;
}
let out = crate::classic3d::draw_run(&mut e, &prep, &canvas).expect("the kernel draws the run");
let img = e.download(&out).unwrap();
let mut cpu = effectcraft_raster::Image::new(W, H);
@@ -253,7 +257,9 @@ fn bokeh_depth_of_field_runs_on_the_gpu() {
assert!(prep.planes.iter().filter(|p| p.draw).all(|p| p.dof.is_some()), "{label}: depth of field deferred to the GPU");
let mut e = crate::context::Enc::new(g.context());
let canvas = e.image(W, H);
assert!(crate::classic3d::draw_run(&mut e, &prep, &canvas).is_some(), "{label}: drawn on the GPU");
if g.context().has_kernel("classic3d") {
assert!(crate::classic3d::draw_run(&mut e, &prep, &canvas).is_some(), "{label}: drawn on the GPU");
}
}
}
}
@@ -288,3 +294,27 @@ fn two_d_layer_with_a_three_d_track_matte() {
}
}
}
/// #613: FXC on DirectX 12 fails to build `classic3d` (it can't unroll its loops). The
/// compositor still starts without it, lists it, and 3D runs render on the CPU, matching the
/// CPU render.
#[test]
fn classic_3d_runs_render_on_the_cpu_without_the_kernel() {
let Some(base) = gpu() else {
return;
};
let ctx = base.context().without_kernel("classic3d");
assert!(!ctx.has_kernel("classic3d"));
let g = crate::Gpu::from_context(ctx);
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = crossing(depth, BlendMode::Normal);
camera(&mut s, [30.0, 20.0, -260.0], None);
let cpu = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Cpu, ..opts() }).comp_frame_cpu(s.cid, Tick::ZERO);
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
r.accel = Some(&g);
let img = g.render(&r, s.cid, Tick::ZERO).expect("the GPU renders the frame");
let mut d = diff(&cpu, &img, tolerance(depth));
d.quantized = depth != BitDepth::Bpc32;
check(&format!("classic 3D without its kernel {depth:?}"), Some(d), 0.0);
}
}