Fix OpenGL startup with portable AO depth reads (#2888) (#2933)

* claim: #2888

* fix: preserve AO depth in a portable integer texture (#2888)
This commit is contained in:
Christian Svensson
2026-10-20 17:41:40 +02:00
committed by GitHub
parent c5129a45d7
commit 511faedcfa
3 changed files with 86 additions and 25 deletions
+13 -5
View File
@@ -29,7 +29,7 @@ fn near_z(c: vec4<f32>) -> f32 {
}
@group(1) @binding(2) var acc_t: texture_2d<f32>;
@group(2) @binding(0) var ao_depth: texture_depth_2d;
@group(2) @binding(0) var ao_depth: texture_2d<u32>;
fn clipped(p: vec3<f32>) -> bool {
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
@@ -141,11 +141,19 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
}
// A single-sample opaque depth map, independent of the window's MSAA and its UI offsets.
// Preserve the depth's bits in R32Uint: GLSL cannot textureLoad a depth texture,
// and float colour attachments are not available on every GL adapter.
struct AoDepth {
@builtin(frag_depth) depth: f32,
@location(0) value: u32,
};
@fragment
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> @builtin(frag_depth) f32 {
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
if (!front && length(u.clip.xyz) > 0.5) { return cap_depth(i.wp, i.pos.z); }
return i.pos.z;
var depth = i.pos.z;
if (!front && length(u.clip.xyz) > 0.5) { depth = cap_depth(i.wp, i.pos.z); }
return AoDepth(depth, bitcast<u32>(depth));
}
// Deterministic cosine-weighted hemisphere samples. Projecting the sample and a point one
@@ -180,7 +188,7 @@ fn occlusion(i: TOut) -> f32 {
let ndc = q.xyz / q.w;
let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
if (any(uv < vec2<f32>(0.0)) || any(uv >= vec2<f32>(1.0)) || ndc.z < 0.0 || ndc.z > 1.0) { continue; }
let d0 = textureLoad(ao_depth, vec2<i32>(uv * vec2<f32>(size)), 0);
let d0 = bitcast<f32>(textureLoad(ao_depth, vec2<i32>(uv * vec2<f32>(size)), 0).x);
let zs = select(ndc.z, 1.0 - ndc.z, flip);
let zn = select(near.z / near.w, 1.0 - near.z / near.w, flip);
let d = select(d0, 1.0 - d0, flip);
+40 -20
View File
@@ -2,6 +2,9 @@
//! depth is drawn once into a single-sample depth map the lit fragment samples for occlusion.
//! The map is independent of the window's MSAA, and the depth runs the same way as the
//! viewport's own buffer (see [`super::depth_compare`] and [`super::far_depth`]).
//! Depth testing chooses the nearest surface; an R32Uint colour attachment stores that depth's
//! bits for portable integer texel reads. GLSL cannot load from a depth texture, and float
//! colour attachments are not available on every GL adapter.
use super::{Batches, color_layout, depth_compare, far_depth, planned_layout, tri_layout};
@@ -9,29 +12,34 @@ pub(super) struct Resources {
pub layout: wgpu::BindGroupLayout,
pub bind: wgpu::BindGroup,
depth: wgpu::TextureView,
values: wgpu::TextureView,
size: [u32; 2],
pipeline: wgpu::RenderPipeline,
planned_pipeline: wgpu::RenderPipeline,
}
fn texture(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, size: [u32; 2]) -> (wgpu::TextureView, wgpu::BindGroup) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("sc_ao_depth"),
size: wgpu::Extent3d { width: size[0], height: size[1], depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth = texture.create_view(&Default::default());
fn texture(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, size: [u32; 2]) -> (wgpu::TextureView, wgpu::TextureView, wgpu::BindGroup) {
let texture = |label, format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width: size[0], height: size[1], depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
};
let depth = texture("sc_ao_depth", wgpu::TextureFormat::Depth32Float, wgpu::TextureUsages::RENDER_ATTACHMENT).create_view(&Default::default());
let values = texture("sc_ao_values", wgpu::TextureFormat::R32Uint, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
.create_view(&Default::default());
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("sc_ao_depth"),
layout,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&depth) }],
entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&values) }],
});
(depth, bind)
(depth, values, bind)
}
impl Resources {
@@ -42,7 +50,7 @@ impl Resources {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
@@ -78,7 +86,11 @@ impl Resources {
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some("fs_ao_depth"),
targets: &[],
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::R32Uint,
blend: None,
write_mask: wgpu::ColorWrites::RED,
})],
compilation_options: Default::default(),
}),
multiview_mask: None,
@@ -87,8 +99,8 @@ impl Resources {
};
let planned_pipeline = pipeline(true);
let pipeline = pipeline(false);
let (depth, bind) = texture(device, &layout, [1, 1]);
Self { layout, bind, depth, size: [1, 1], pipeline, planned_pipeline }
let (depth, values, bind) = texture(device, &layout, [1, 1]);
Self { layout, bind, depth, values, size: [1, 1], pipeline, planned_pipeline }
}
pub fn prepare(
@@ -105,7 +117,7 @@ impl Resources {
// Bound memory at large window sizes; normalized coordinates also handle downsampling.
let size = target_size(enabled, size_px, device.limits().max_texture_dimension_2d);
if size != self.size {
(self.depth, self.bind) = texture(device, &self.layout, size);
(self.depth, self.values, self.bind) = texture(device, &self.layout, size);
self.size = size;
}
if !enabled {
@@ -113,7 +125,15 @@ impl Resources {
}
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("sc_ao_prepass"),
color_attachments: &[],
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.values,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: f64::from(far_depth(reversed).to_bits()), g: 0.0, b: 0.0, a: 0.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth,
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(far_depth(reversed)), store: wgpu::StoreOp::Store }),
+33
View File
@@ -39,6 +39,39 @@ fn session(script: &[(&str, serde_json::Value)]) -> Session {
s
}
/// #2888: compiling the viewport on GL used to reject AO's depth textureLoad, even
/// with AO disabled. Select GL explicitly so a working Vulkan adapter cannot hide it.
#[test]
fn opengl_viewport_pipelines_render_a_model() {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = wgpu::Backends::GL;
let instance = wgpu::Instance::new(desc);
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
Ok(adapter) => adapter,
Err(e) => {
let required = std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_some() && wgpu::Backends::from_env() == Some(wgpu::Backends::GL);
assert!(!required, "OpenGL is required here: {e}");
eprintln!("skipping the OpenGL regression: {e}");
return;
}
};
assert_eq!(adapter.get_info().backend, wgpu::Backend::Gl);
eprintln!("OpenGL regression: {:?}", adapter.get_info());
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_features: solidcraft_gpu::depth_features(&adapter),
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
..Default::default()
}))
.unwrap();
let c = Capturer::new(&adapter, device, queue).unwrap();
let s = session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20}))]);
let cam = view::home_camera(&s);
let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }), &cam, 160, 100).unwrap();
let empty = c.rgba(&Default::default(), &cam, 160, 100).unwrap();
let changed = pixels(&img).zip(pixels(&empty)).filter(|(a, b)| a != b).count();
assert!(changed > 500, "the model covers only {changed} pixels");
}
/// Appearances show as the viewport shows them: a body's colour, a face's own look.
#[test]
fn pictures_show_appearances() {