Merge pull request #1571 from MatejKozlovsky/dx12-fxc-kernels

GPU: Classic 3D builds with FXC, so the GPU compositor starts on DirectX 12 (#613)
This commit is contained in:
BFlat
2026-10-29 10:11:21 +07:00
committed by GitHub
5 changed files with 119 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() {
+64 -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;
@@ -622,15 +623,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)
@@ -927,6 +934,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) {
@@ -934,6 +946,49 @@ 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(),
software: self.software,
yuv: std::sync::OnceLock::new(),
}
}
/// 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);
}
}