mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
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:
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user