Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098) Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error, and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline with D3D12's FXC on the WARP adapter. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098) FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array (ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step offsets computed rather than indexed. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a6928abf48
commit
ec64564f21
9 files changed
+329
-86
No files matched your search
+47
-16
@@ -736,14 +736,30 @@ impl Batches {
|
||||
}
|
||||
}
|
||||
|
||||
/// The tessellation pipeline (`shader`: its source), where display plans are evaluated: on a
|
||||
/// device that runs compute shaders with the storage buffers the evaluation binds (not WebGL2)
|
||||
/// and builds the pipeline. Elsewhere `None`, and bodies are built on the CPU: a device whose
|
||||
/// shader compiler rejects the shader (D3D12's FXC once did) only loses GPU evaluation.
|
||||
fn tessellation(adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, shader: &str) -> Option<solidcraft_gpu::TessGpu> {
|
||||
let compute = adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
|
||||
&& device.limits().max_storage_buffers_per_shader_stage >= 4;
|
||||
if !compute {
|
||||
return None;
|
||||
}
|
||||
let info = adapter.get_info();
|
||||
match solidcraft_gpu::TessGpu::with_shader(info.name.clone(), device.clone(), queue.clone(), shader) {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
log::warn!("bodies are built on the CPU on {} ({:?}): {e}", info.name, info.backend);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the pipelines. Call once with the app's render state and its depth/stencil/MSAA
|
||||
/// settings. Without a stencil the section cap does not hide the inside of cut bodies.
|
||||
pub fn install(rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) -> GpuTarget {
|
||||
// Display plans are evaluated where the device runs compute shaders with the storage
|
||||
// buffers the evaluation binds (not WebGL2): elsewhere bodies are built on the CPU.
|
||||
let compute = rs.adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
|
||||
&& rs.device.limits().max_storage_buffers_per_shader_stage >= 4;
|
||||
let tess = compute.then(|| solidcraft_gpu::TessGpu::new(rs.adapter.get_info().name, rs.device.clone(), rs.queue.clone()));
|
||||
let tess = tessellation(&rs.adapter, &rs.device, &rs.queue, solidcraft_gpu::SHADER);
|
||||
let t = GpuTarget { format: rs.target_format, depth: egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits), samples, tess: tess.is_some() };
|
||||
let res = Resources::new(&rs.device, t, tess);
|
||||
rs.renderer.write().callback_resources.insert(res);
|
||||
@@ -912,8 +928,29 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
|
||||
/// Render `scene` offscreen on a device of its own, through the viewport's real resources and
|
||||
/// frame callback (`prepare`, then `paint`), and read the pixels back: straight RGBA, `size` ×
|
||||
/// `size`. `None` without a GPU adapter. With `tess`, planned bodies are evaluated on the GPU.
|
||||
/// `size`. `None` without a GPU adapter. With `tess` (the tessellation shader), planned bodies
|
||||
/// are evaluated on the GPU where the device builds that pipeline, as [`install`] sets it up.
|
||||
/// `highlight` is drawn as the viewport's highlight scene (hover and selection).
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
fn offscreen_device() -> Option<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||
..Default::default()
|
||||
}))
|
||||
.ok()?;
|
||||
Some((adapter, device, queue))
|
||||
}
|
||||
|
||||
/// Whether [`install`] would evaluate plans on the GPU with `shader`, on an offscreen device
|
||||
/// (`None` without a GPU adapter).
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn offscreen_tessellates(shader: &str) -> Option<bool> {
|
||||
let (adapter, device, queue) = offscreen_device()?;
|
||||
Some(tessellation(&adapter, &device, &queue, shader).is_some())
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn render_offscreen(
|
||||
scene: GpuScene,
|
||||
@@ -922,18 +959,12 @@ pub(crate) fn render_offscreen(
|
||||
back: [f32; 3],
|
||||
eye: [f32; 4],
|
||||
size: u32,
|
||||
tess: bool,
|
||||
tess: Option<&str>,
|
||||
) -> Option<Vec<u8>> {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||
..Default::default()
|
||||
}))
|
||||
.ok()?;
|
||||
let (adapter, device, queue) = offscreen_device()?;
|
||||
let (format, depth) = (wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::Depth24PlusStencil8);
|
||||
let target = GpuTarget { format, depth: Some(depth), samples: 1, tess };
|
||||
let compute = tess.then(|| solidcraft_gpu::TessGpu::new(adapter.get_info().name, device.clone(), queue.clone()));
|
||||
let compute = tess.and_then(|shader| tessellation(&adapter, &device, &queue, shader));
|
||||
let target = GpuTarget { format, depth: Some(depth), samples: 1, tess: compute.is_some() };
|
||||
let mut resources = egui_wgpu::CallbackResources::default();
|
||||
resources.insert(Resources::new(&device, target, compute));
|
||||
let none = || Arc::new(Mutex::new(None));
|
||||
|
||||
@@ -3316,8 +3316,8 @@ mod tests {
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
let planned = scene.planned.len();
|
||||
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, None, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess))
|
||||
let (planned, shader) = (scene.planned.len(), tess.then_some(solidcraft_gpu::SHADER));
|
||||
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, None, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, shader))
|
||||
};
|
||||
let (planned, gpu_tris, gpu) = render(true);
|
||||
let (none, cpu_tris, cpu) = render(false);
|
||||
@@ -3335,6 +3335,45 @@ mod tests {
|
||||
assert!(differ * 100 < drawn, "{differ} of {drawn} model pixels differ");
|
||||
}
|
||||
|
||||
/// A device that cannot build the tessellation pipeline (D3D12's FXC rejected the shader
|
||||
/// at startup and the app panicked) still shows the model: the failure is caught, logged,
|
||||
/// and bodies are built on the CPU and drawn. A shader the device rejects stands in for FXC.
|
||||
/// Skipped without a GPU adapter.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn bodies_are_built_on_the_cpu_when_the_tessellation_pipeline_fails() {
|
||||
const REJECTED: &str = "
|
||||
@group(0) @binding(1) var<storage, read_write> surfaces: array<u32>;
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) gid: vec3<u32>) { surfaces[gid.x] = 1u; }
|
||||
";
|
||||
let Some(tess) = crate::gpu::offscreen_tessellates(REJECTED) else {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
assert!(!tess, "the rejected shader built a pipeline");
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.run_script(&solidcraft_engine::sample::script()).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
let scene = build_scene(&app);
|
||||
assert!(scene.planned.is_empty(), "no body waits for the GPU evaluation");
|
||||
assert!(!scene.tris.is_empty(), "the bodies are meshed on the CPU");
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
let Some(img) = crate::gpu::render_offscreen(scene, None, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, Some(REJECTED)) else {
|
||||
return;
|
||||
};
|
||||
let n = (SIZE * SIZE) as usize;
|
||||
let drawn = img.chunks(4).filter(|p| p[..3] != [255, 255, 255]).count();
|
||||
assert!(drawn > n / 20, "the model is drawn: {drawn} pixels");
|
||||
}
|
||||
|
||||
/// A hovered face lights solidly over a body drawn from its display plan on the GPU: the
|
||||
/// highlight uses the plan's triangles, not the reference mesh's other triangulation of the
|
||||
/// same face, which fought the model at equal depth and shimmered in wedges. Every face of a
|
||||
@@ -3362,7 +3401,8 @@ mod tests {
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
crate::gpu::render_offscreen(scene, Some(highlight), proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess)
|
||||
let shader = tess.then_some(solidcraft_gpu::SHADER);
|
||||
crate::gpu::render_offscreen(scene, Some(highlight), proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, shader)
|
||||
};
|
||||
let lit = |plain: &[u8], hovered: &[u8]| -> Vec<bool> { plain.chunks(4).zip(hovered.chunks(4)).map(|(a, b)| a != b).collect() };
|
||||
let (Some(gpu_plain), Some(cpu_plain)) = (render(true, None), render(false, None)) else {
|
||||
|
||||
Reference in new issue
Block a user