mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-01 22:07:28 -06:00
GPU: fs_adjust compiles under FXC (D3D12); a pipeline build failure falls back to the CPU
On D3D12 without DXC, wgpu compiles HLSL with FXC, which aborted on
fs_adjust ("internal error: compilation aborted unexpectedly"). wgpu turns
that into a panic, so every GPU test failed on the Windows runner, and the
app would crash where the same path is taken. A bisect on the Windows
runner pinned it to two cases of adjust()'s switch, Exposure and Selective
Color: both looped over dynamically indexed vector components. They are
now written per channel / per range with the same math (GPU-vs-CPU parity
is unchanged and passes on Metal and on the Windows runner).
Compositor::try_new_with_format catches validation and internal errors
while building the pipelines and returns Err; the canvas then uses the CPU
compositor (once, remembered) instead of crashing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1c09936154
commit
71272d5cd7
+27
-30
@@ -402,35 +402,39 @@ fn color_lookup(c: vec3<f32>, n: i32, tetra: bool) -> vec3<f32> {
|
||||
return mix(mix(c00, c10, f.y), mix(c01, c11, f.y), f.z);
|
||||
}
|
||||
|
||||
// compose::adjust::selective_color (ranges × CMYK percentages in LUT row 0)
|
||||
// One Selective Color range: its CMYK percentages (LUT row 0, 4 per range) weighted by `w`.
|
||||
fn selective_row(r: i32, w: f32, c: vec3<f32>, relative: bool) -> vec3<f32> {
|
||||
let k = lut_at(r * 4 + 3) / 100.0;
|
||||
var d = vec3(lut_at(r * 4), lut_at(r * 4 + 1), lut_at(r * 4 + 2)) / 100.0 + k;
|
||||
if (relative) { d = d * (1.0 - c); }
|
||||
return select(vec3(0.0), d * w, w > 0.0);
|
||||
}
|
||||
|
||||
// compose::adjust::selective_color (ranges × CMYK percentages in LUT row 0). Written without
|
||||
// loops: FXC (D3D12) aborts compiling loops with dynamic indexing inside `adjust`'s switch.
|
||||
fn selective_color(c: vec3<f32>, relative: bool) -> vec3<f32> {
|
||||
let mx = max(max(c.r, c.g), c.b);
|
||||
let mn = min(min(c.r, c.g), c.b);
|
||||
let md = c.r + c.g + c.b - mx - mn;
|
||||
var w: array<f32, 9>;
|
||||
w[0] = select(0.0, mx - md, c.r >= mx);
|
||||
w[1] = select(0.0, md - mn, c.b <= mn);
|
||||
w[2] = select(0.0, mx - md, c.g >= mx);
|
||||
w[3] = select(0.0, md - mn, c.r <= mn);
|
||||
w[4] = select(0.0, mx - md, c.b >= mx);
|
||||
w[5] = select(0.0, md - mn, c.g <= mn);
|
||||
w[6] = max((mn - 0.5) * 2.0, 0.0);
|
||||
w[7] = clamp(1.0 - abs(mx - 0.5) - abs(mn - 0.5), 0.0, 1.0);
|
||||
w[8] = max((0.5 - mx) * 2.0, 0.0);
|
||||
var delta = vec3(0.0);
|
||||
for (var r = 0; r < 9; r++) {
|
||||
if (w[r] <= 0.0) { continue; }
|
||||
let k = lut_at(r * 4 + 3) / 100.0;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let a = lut_at(r * 4 + i) / 100.0;
|
||||
var d = a + k;
|
||||
if (relative) { d = d * (1.0 - c[i]); }
|
||||
delta[i] += d * w[r];
|
||||
}
|
||||
}
|
||||
var delta = selective_row(0, select(0.0, mx - md, c.r >= mx), c, relative);
|
||||
delta += selective_row(1, select(0.0, md - mn, c.b <= mn), c, relative);
|
||||
delta += selective_row(2, select(0.0, mx - md, c.g >= mx), c, relative);
|
||||
delta += selective_row(3, select(0.0, md - mn, c.r <= mn), c, relative);
|
||||
delta += selective_row(4, select(0.0, mx - md, c.b >= mx), c, relative);
|
||||
delta += selective_row(5, select(0.0, md - mn, c.g <= mn), c, relative);
|
||||
delta += selective_row(6, max((mn - 0.5) * 2.0, 0.0), c, relative);
|
||||
delta += selective_row(7, clamp(1.0 - abs(mx - 0.5) - abs(mn - 0.5), 0.0, 1.0), c, relative);
|
||||
delta += selective_row(8, max((0.5 - mx) * 2.0, 0.0), c, relative);
|
||||
return clamp(c - delta, vec3(0.0), vec3(1.0));
|
||||
}
|
||||
|
||||
// Exposure on one channel (p = exposure scale, offset, gamma, transfer gamma). Per channel, not a
|
||||
// loop over `c[i]`: FXC aborts on that inside `adjust`'s switch.
|
||||
fn exposure(v: f32, p: vec4<f32>) -> f32 {
|
||||
let lin = pow(max(t_decode(v, p.w) * p.x + p.y, 0.0), 1.0 / p.z);
|
||||
return clamp(t_encode(lin, p.w), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Modern Brightness curve: line of slope 1.375^(b/50) rolled off to (1,1) by a v^P white anchor.
|
||||
// Mirrors compose::adjust::modern_brightness.
|
||||
fn mbright(v: f32, b: f32) -> f32 {
|
||||
@@ -472,14 +476,7 @@ fn adjust(c: vec3<f32>) -> vec3<f32> {
|
||||
let b = p0.x; let ct = p0.y;
|
||||
return vec3(mcontrast(mbright(c.r, b), ct), mcontrast(mbright(c.g, b), ct), mcontrast(mbright(c.b, b), ct));
|
||||
}
|
||||
case 6: { // Exposure
|
||||
var o: vec3<f32>;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let lin = pow(max(t_decode(c[i], p0.w) * p0.x + p0.y, 0.0), 1.0 / p0.z);
|
||||
o[i] = clamp(t_encode(lin, p0.w), 0.0, 1.0);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
case 6: { return vec3(exposure(c.r, p0), exposure(c.g, p0), exposure(c.b, p0)); } // Exposure
|
||||
case 7: { return vec3(lut(0, c.r), lut(1, c.g), lut(2, c.b)); } // Levels / Curves
|
||||
case 8: { // Hue/Saturation
|
||||
let hsl = rgb_to_hsl(c);
|
||||
|
||||
@@ -394,6 +394,25 @@ impl Compositor {
|
||||
if feats.allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) { wgpu::TextureFormat::Rgba32Float } else { wgpu::TextureFormat::Rgba16Float }
|
||||
}
|
||||
|
||||
/// [`Compositor::new_with_format`], but a shader or pipeline the driver can't build (e.g. a
|
||||
/// D3D12 shader-compiler failure) is an `Err` instead of a panic, so the caller can fall back
|
||||
/// to the CPU compositor. Where errors only arrive asynchronously (WebGPU), creation is assumed
|
||||
/// to have worked.
|
||||
pub fn try_new_with_format(device: &wgpu::Device, acc_format: wgpu::TextureFormat) -> Result<Self, Unsupported> {
|
||||
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
|
||||
let comp = Self::new_with_format(device, acc_format);
|
||||
// Pop in reverse order; native wgpu reports errors synchronously, so the futures are ready.
|
||||
let errors = [internal.pop(), validation.pop()];
|
||||
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
|
||||
for f in errors {
|
||||
if let std::task::Poll::Ready(Some(e)) = std::pin::pin!(f).poll(&mut cx) {
|
||||
return Err(Unsupported(format!("couldn't build the compositor's pipelines: {e}")));
|
||||
}
|
||||
}
|
||||
Ok(comp)
|
||||
}
|
||||
|
||||
/// Create a compositor whose accumulation/render-target format is `acc_format`. Pass
|
||||
/// `Rgba16Float` on adapters that can't render to `Rgba32Float` (e.g. some Intel Vulkan drivers),
|
||||
/// which otherwise panics at render-pipeline creation. See `Compositor::preferred_acc_format`.
|
||||
|
||||
@@ -1120,3 +1120,13 @@ fn rgba16f_fallback_path_renders() {
|
||||
let f = Compositor::preferred_acc_format(&adapter);
|
||||
assert!(matches!(f, wgpu::TextureFormat::Rgba32Float | wgpu::TextureFormat::Rgba16Float));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbuildable_pipelines_are_an_error_not_a_panic() {
|
||||
// The app falls back to the CPU compositor on Err; a panic here would crash it (as FXC once
|
||||
// did on D3D12). A depth format can't be a colour target, so its pipelines fail to build.
|
||||
let Some(g) = gpu() else { return };
|
||||
let e = Compositor::try_new_with_format(&g.device, wgpu::TextureFormat::Depth32Float).err().expect("depth target must fail");
|
||||
assert!(e.0.contains("pipelines"), "{e}");
|
||||
assert!(Compositor::try_new_with_format(&g.device, wgpu::TextureFormat::Rgba16Float).is_ok());
|
||||
}
|
||||
|
||||
@@ -160,10 +160,21 @@ impl GpuCanvas {
|
||||
}
|
||||
let mut renderer = self.rs.renderer.write();
|
||||
let Some(res) = renderer.callback_resources.get_mut::<Resources>() else { return Err(photocraft_gpu::Unsupported("no GPU canvas".into())) };
|
||||
let mut comp = res
|
||||
.compositor
|
||||
.take()
|
||||
.unwrap_or_else(|| photocraft_gpu::Compositor::new_with_format(device, photocraft_gpu::Compositor::preferred_acc_format(&self.rs.adapter)));
|
||||
// A driver that couldn't build the pipelines once won't later: stay on the CPU compositor.
|
||||
if let Some(e) = &res.compositor_failed {
|
||||
return Err(e.clone());
|
||||
}
|
||||
let mut comp = match res.compositor.take() {
|
||||
Some(c) => c,
|
||||
None => match photocraft_gpu::Compositor::try_new_with_format(device, photocraft_gpu::Compositor::preferred_acc_format(&self.rs.adapter)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::warn!("{e}; using the CPU compositor");
|
||||
res.compositor_failed = Some(e.clone());
|
||||
return Err(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
let key = doc.id.0;
|
||||
let fresh = res.docs.get(&key).is_none_or(|d| d.size != size);
|
||||
let region = if fresh { doc.bounds() } else { region.intersect(&doc.bounds()) };
|
||||
@@ -382,6 +393,8 @@ struct Resources {
|
||||
out_linear: bool,
|
||||
/// The wgpu layer compositor (created on first use).
|
||||
compositor: Option<photocraft_gpu::Compositor>,
|
||||
/// Why the wgpu compositor couldn't be created (then the CPU compositor is used).
|
||||
compositor_failed: Option<photocraft_gpu::Unsupported>,
|
||||
encode_bgl: wgpu::BindGroupLayout,
|
||||
encode_pipeline: wgpu::RenderPipeline,
|
||||
lut_bgl: wgpu::BindGroupLayout,
|
||||
@@ -632,6 +645,7 @@ impl Resources {
|
||||
views: HashMap::new(),
|
||||
out_linear: target.is_srgb(),
|
||||
compositor: None,
|
||||
compositor_failed: None,
|
||||
encode_bgl,
|
||||
encode_pipeline,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user