Skip to content

Commit fdf3a39

Browse files
committed
WIP scratch: GL debug (not for merge) gl allowance
1 parent e2f027f commit fdf3a39

21 files changed

Lines changed: 272 additions & 53 deletions

‎.github/workflows/freebsd.yml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Builds and tests the workspace on FreeBSD, inside a FreeBSD VM on a Linux runner
2+
# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
3+
name: FreeBSD
4+
5+
on:
6+
push:
7+
branches: [main]
8+
pull_request:
9+
paths:
10+
- Cargo.toml
11+
- Cargo.lock
12+
- "**/Cargo.toml"
13+
- .github/workflows/freebsd.yml
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: freebsd-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
freebsd:
25+
name: FreeBSD 14 (build + test)
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 300
28+
steps:
29+
- uses: actions/checkout@v4
30+
- name: Build and test in a FreeBSD VM
31+
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1
32+
with:
33+
release: "14.3"
34+
usesh: true
35+
cpu: 4
36+
mem: 12288
37+
# Nothing to bring back; copying the target dir out of the VM fails on large builds.
38+
copyback: false
39+
prepare: |
40+
pkg install -y git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
41+
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
42+
run: |
43+
. "$HOME/.cargo/env"
44+
# The checkout is synced in under another owner; let tests that call git read it.
45+
git config --global --add safe.directory "*"
46+
rustc --version
47+
CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked
48+
CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked

‎.github/workflows/gl-debug.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# TEMPORARY (scratch branch only): the gpu crate's tests on wgpu's GL backend (Mesa llvmpipe).
2+
name: GL debug
3+
on:
4+
push:
5+
branches: [scratch/gl-debug]
6+
permissions:
7+
contents: read
8+
jobs:
9+
gl:
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 90
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Mesa (GL only)
15+
run: |
16+
sudo apt-get update -qq
17+
sudo apt-get install -y -qq libegl1 libegl-mesa0 libgl1-mesa-dri libgles2 libgbm1 libxkbcommon-dev libwayland-dev libasound2-dev libudev-dev libgtk-3-dev
18+
sudo apt-get remove -y -qq mesa-vulkan-drivers || true
19+
ls /usr/share/vulkan/icd.d 2>/dev/null || true
20+
- uses: Swatinem/rust-cache@v2
21+
- name: gpu tests on GL
22+
env:
23+
CARGO_PROFILE_DEV_DEBUG: "0"
24+
EC_GL_DEBUG: "1"
25+
run: |
26+
rustc --version
27+
cargo test -p effectcraft-gpu --locked backend_selection -- --nocapture 2>&1 | grep -E "adapter:|test result"
28+
set -o pipefail
29+
cargo test -p effectcraft-gpu --locked 2>&1 | tee gl.log

‎crates/gpu/src/adv3d.rs‎

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! (`Depth32Float`, Greater), the scene's materials, textures, lights and shadow maps in
33
//! storage buffers, and `advanced3d.wgsl`'s physically based fragment shader. Opaque triangles
44
//! are drawn with depth writes, then the sorted transparent ones blended over (premultiplied).
5-
//! Colour (`Rgba16Float`) and camera depth (`R32Float`) stay on the GPU for `adv3d.wgsl`'s
5+
//! Colour (`Rgba16Float`) and camera depth (`R32Uint` holding f32 bits) stay on the GPU for `adv3d.wgsl`'s
66
//! compute kernels, which run the rest of `effectcraft_render::three_d::adv::render_prepared`:
77
//! the 2×2 supersampling resolve, the motion-blur sub-sample average (nearest depth), the
88
//! depth-based iris depth of field (the Classic 3D bokeh kernel's row spans and prefix-sum
@@ -22,20 +22,44 @@ use crate::context::{Enc, GpuContext, GpuImage};
2222
/// Pipelines (built on first use).
2323
pub(crate) struct Pipes {
2424
bgl: wgpu::BindGroupLayout,
25-
opaque: wgpu::RenderPipeline,
26-
transparent: wgpu::RenderPipeline,
25+
/// (opaque, transparent); `None` when the adapter can't rasterise ([`raster_unsupported`]):
26+
/// scenes then render on the CPU.
27+
raster: Option<(wgpu::RenderPipeline, wgpu::RenderPipeline)>,
2728
post: Post,
2829
}
2930

3031
fn pipes(g: &GpuContext) -> &Pipes {
31-
g.adv3d.get_or_init(|| Pipes::new(&g.device))
32+
g.adv3d.get_or_init(|| Pipes::new(&g.device, g.adv3d_raster))
3233
}
3334

3435
const COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
35-
const DEPTH_OUT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
36+
/// Camera depth as the bits of an f32: integer targets render on every backend, while `R32Float`
37+
/// needs `EXT_color_buffer_float` on GLES (absent on Mesa's llvmpipe, for one).
38+
const DEPTH_OUT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
39+
/// [`DEPTH_OUT`]'s clear value: the bits of −1.0 ("nothing drawn").
40+
const NO_DEPTH: f64 = 0xBF80_0000u32 as f64;
3641
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
3742
const VERTEX_SIZE: u64 = 52;
3843

44+
/// Why `adapter` can't run the rasteriser, or `None` when it can. Building the pipelines anyway
45+
/// would be a validation error, which release builds only log (and then draw nothing), so the
46+
/// caller keeps the scenes on the CPU instead.
47+
pub(crate) fn raster_unsupported(adapter: &wgpu::Adapter) -> Option<String> {
48+
use wgpu::TextureFormatFeatureFlags as F;
49+
let needs = [(COLOR, F::BLENDABLE), (DEPTH_OUT, F::empty()), (DEPTH, F::empty())];
50+
for (format, flags) in needs {
51+
let f = adapter.get_texture_format_features(format);
52+
if !f.allowed_usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) || !f.flags.contains(flags) {
53+
return Some(format!("{format:?} is not a {}render target", if flags.is_empty() { "" } else { "blendable " }));
54+
}
55+
}
56+
// The transparent pipeline blends colour and masks the depth target off.
57+
if !adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::INDEPENDENT_BLEND) {
58+
return Some("no independent blending".into());
59+
}
60+
None
61+
}
62+
3963
fn storage(binding: u32) -> wgpu::BindGroupLayoutEntry {
4064
wgpu::BindGroupLayoutEntry {
4165
binding,
@@ -46,7 +70,7 @@ fn storage(binding: u32) -> wgpu::BindGroupLayoutEntry {
4670
}
4771

4872
impl Pipes {
49-
fn new(device: &wgpu::Device) -> Pipes {
73+
fn new(device: &wgpu::Device, raster: bool) -> Pipes {
5074
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
5175
label: Some("effectcraft advanced 3d"),
5276
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/advanced3d.wgsl").into()),
@@ -110,7 +134,7 @@ impl Pipes {
110134
cache: None,
111135
})
112136
};
113-
Pipes { opaque: make(false), transparent: make(true), bgl, post: Post::new(device) }
137+
Pipes { raster: raster.then(|| (make(false), make(true))), bgl, post: Post::new(device) }
114138
}
115139
}
116140

@@ -222,9 +246,9 @@ fn half_to_f32(h: u16) -> f32 {
222246
}
223247
}
224248

225-
/// Record the render pass of one scene into `e`: (colour `Rgba16Float`, camera depth `R32Float`
249+
/// Record the render pass of one scene into `e`: (colour `Rgba16Float`, camera depth `R32Uint`
226250
/// with −1 where nothing was drawn) at the scene's raster size. `None` when the device can't
227-
/// (size or buffer limits).
251+
/// (render targets, size or buffer limits).
228252
fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)> {
229253
let g = e.g;
230254
if !g.fits(s.width, s.height) || s.indices.is_empty() {
@@ -236,6 +260,7 @@ fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)>
236260
return None;
237261
}
238262
let p = pipes(g);
263+
let Some((opaque, transparent)) = &p.raster else { return None };
239264
let dev = &g.device;
240265
let vb: Vec<u8> = s
241266
.vertices
@@ -294,7 +319,7 @@ fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)>
294319
view: &zv,
295320
depth_slice: None,
296321
resolve_target: None,
297-
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: -1.0, g: 0.0, b: 0.0, a: 0.0 }), store: wgpu::StoreOp::Store },
322+
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: NO_DEPTH, g: 0.0, b: 0.0, a: 0.0 }), store: wgpu::StoreOp::Store },
298323
}),
299324
],
300325
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
@@ -311,11 +336,11 @@ fn raster_into(e: &mut Enc, s: &Scene) -> Option<(wgpu::Texture, wgpu::Texture)>
311336
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
312337
let n = s.indices.len() as u32;
313338
if s.opaque_count > 0 {
314-
pass.set_pipeline(&p.opaque);
339+
pass.set_pipeline(opaque);
315340
pass.draw_indexed(0..s.opaque_count, 0, 0..1);
316341
}
317342
if n > s.opaque_count {
318-
pass.set_pipeline(&p.transparent);
343+
pass.set_pipeline(transparent);
319344
pass.draw_indexed(s.opaque_count..n, 0, 0..1);
320345
}
321346
}
@@ -424,11 +449,11 @@ impl Post {
424449
label: Some("effectcraft advanced 3d post"),
425450
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/adv3d.wgsl").into()),
426451
});
427-
let tex = |binding| wgpu::BindGroupLayoutEntry {
452+
let tex = |binding, sample_type| wgpu::BindGroupLayoutEntry {
428453
binding,
429454
visibility: wgpu::ShaderStages::COMPUTE,
430455
ty: wgpu::BindingType::Texture {
431-
sample_type: wgpu::TextureSampleType::Float { filterable: false },
456+
sample_type,
432457
view_dimension: wgpu::TextureViewDimension::D2,
433458
multisampled: false,
434459
},
@@ -447,8 +472,8 @@ impl Post {
447472
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
448473
count: None,
449474
},
450-
tex(1),
451-
tex(2),
475+
tex(1, wgpu::TextureSampleType::Float { filterable: false }),
476+
tex(2, wgpu::TextureSampleType::Uint),
452477
buf(3, false),
453478
buf(4, false),
454479
buf(5, false),

‎crates/gpu/src/context.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ pub struct GpuContext {
259259
zeros: Mutex<HashMap<(u32, u32), GpuImage>>,
260260
/// Advanced 3D render pipelines (built on first use).
261261
pub(crate) adv3d: std::sync::OnceLock<crate::adv3d::Pipes>,
262+
/// The adapter can run the Advanced 3D rasteriser (`adv3d::raster_unsupported`); without
263+
/// it Advanced 3D scenes render on the CPU.
264+
pub(crate) adv3d_raster: bool,
262265
/// GPU particle pipeline (built on first use) and simulation checkpoints.
263266
pub(crate) particles: crate::particles::PipesCell,
264267
pub(crate) particle_states: crate::particles::StatesCell,
@@ -303,6 +306,10 @@ impl GpuContext {
303306
}
304307
}
305308
let name = format!("{} ({:?})", info.name, info.backend);
309+
let adv3d_unsupported = crate::adv3d::raster_unsupported(adapter);
310+
if let Some(why) = &adv3d_unsupported {
311+
log::info!("gpu {name}: Advanced 3D renders on the CPU ({why})");
312+
}
306313
let src = [
307314
include_str!("shaders/common.wgsl"),
308315
include_str!("shaders/kernels.wgsl"),
@@ -466,6 +473,7 @@ impl GpuContext {
466473
transfers: Default::default(),
467474
zeros: Default::default(),
468475
adv3d: std::sync::OnceLock::new(),
476+
adv3d_raster: adv3d_unsupported.is_none(),
469477
particles: Default::default(),
470478
particle_states: Default::default(),
471479
deferred: None,

‎crates/gpu/src/shaders/adv3d.wgsl‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ struct Post {
2525

2626
@group(0) @binding(0) var<uniform> P: Post;
2727
@group(0) @binding(1) var color_in: texture_2d<f32>;
28-
@group(0) @binding(2) var z_in: texture_2d<f32>;
28+
// Camera depth as f32 bits (an `R32Uint` target: integer targets render on every backend,
29+
// `R32Float` doesn't on GLES without EXT_color_buffer_float).
30+
@group(0) @binding(2) var z_in: texture_2d<u32>;
2931
@group(0) @binding(3) var<storage, read_write> acc: array<vec4<f32>>;
3032
@group(0) @binding(4) var<storage, read_write> depth: array<f32>;
3133
@group(0) @binding(5) var<storage, read_write> src: array<vec4<f32>>;
@@ -55,7 +57,7 @@ fn resolve(@builtin(global_invocation_id) id: vec3<u32>) {
5557
for (var i = 0u; i < k; i = i + 1u) {
5658
let p = vec2<i32>(i32(id.x * k + i), i32(id.y * k + j));
5759
a = a + textureLoad(color_in, p, 0);
58-
let z = textureLoad(z_in, p, 0).x;
60+
let z = bitcast<f32>(textureLoad(z_in, p, 0).x);
5961
if (z >= 0.0) {
6062
dz = dz + z;
6163
dn = dn + 1.0;

‎crates/gpu/src/shaders/advanced3d.wgsl‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,28 @@ fn srgb_to_linear(c: f32) -> f32 {
7575
return pow((c + 0.055) / 1.055, 2.4);
7676
}
7777

78+
// `a` mod `n` in [0, n) for n > 0 (floored). Never `%` on a negative operand: WGSL's `%` truncates,
79+
// but naga's GLSL output is plain `%`, undefined for negative operands (wrong on Mesa's GL).
80+
fn imod(a: i32, n: i32) -> i32 {
81+
if (a >= 0) {
82+
return a % n;
83+
}
84+
return n - 1 - (-(a + 1)) % n;
85+
}
86+
7887
fn wrap_i(i: i32, n: i32, mode: u32) -> i32 {
7988
if (mode == 1u) {
8089
return clamp(i, 0, n - 1);
8190
}
8291
if (mode == 2u) {
8392
let p = 2 * n;
84-
var m = i % p;
85-
if (m < 0) {
86-
m = m + p;
87-
}
93+
let m = imod(i, p);
8894
if (m >= n) {
8995
return p - 1 - m;
9096
}
9197
return m;
9298
}
93-
var m = i % n;
94-
if (m < 0) {
95-
m = m + n;
96-
}
97-
return m;
99+
return imod(i, n);
98100
}
99101

100102
fn texel(t: vec4<u32>, x: i32, y: i32) -> vec4<f32> {
@@ -282,7 +284,8 @@ fn attenuation(kind: u32, falloff: u32, radius: f32, falloff_distance: f32, cos_
282284

283285
struct FOut {
284286
@location(0) color: vec4<f32>,
285-
@location(1) depth: f32,
287+
// Camera depth's f32 bits (an `R32Uint` target, see `adv3d.rs`).
288+
@location(1) depth: u32,
286289
};
287290

288291
@fragment
@@ -326,7 +329,7 @@ fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
326329
emissive = emissive * sample_tex(tex_em, i.uv).rgb;
327330
}
328331
var out: FOut;
329-
out.depth = dot(g.view_z.xyz, i.pos) + g.view_z.w;
332+
out.depth = bitcast<u32>(dot(g.view_z.xyz, i.pos) + g.view_z.w);
330333
if (unlit || !accepts_lights || g.env2.y < 0.5) {
331334
let c = rgb + emissive;
332335
out.color = vec4<f32>(c * alpha, alpha);

‎crates/gpu/src/shaders/classic3d.wgsl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ fn attenuation(l: u32, p: vec3<f32>) -> f32 {
186186
}
187187
if (li.ints.x == 3u) {
188188
let cos_a = clamp(dot(-tl.xyz, li.dir.xyz), -1.0, 1.0);
189-
let a = acos(cos_a);
189+
let a = acos_p(cos_a);
190190
let outer = li.cone.x;
191191
let inner = outer * (1.0 - li.cone.y);
192192
if (a <= inner) {

‎crates/gpu/src/shaders/common.wgsl‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ struct Params {
1616
@group(0) @binding(2) var aux: texture_2d<f32>;
1717
@group(0) @binding(4) var<storage, read> data: array<f32>;
1818

19+
// `a` mod `n` in [0, n) for n > 0 (floored). Never `%` on a negative operand: WGSL's `%` truncates,
20+
// but naga's GLSL output is plain `%`, undefined for negative operands (wrong on Mesa's GL).
21+
fn imod(a: i32, n: i32) -> i32 {
22+
if (a >= 0) {
23+
return a % n;
24+
}
25+
return n - 1 - (-(a + 1)) % n;
26+
}
27+
28+
// asin / acos to f32 precision on every backend (Cephes' asinf polynomial). WGSL leaves their
29+
// precision to the implementation, and Mesa's GL drivers use a coarse approximation (errors near
30+
// 1e-4 rad), which shows in every effect that maps through a sphere or a lens.
31+
fn asin_p(x: f32) -> f32 {
32+
let a = abs(x);
33+
let big = a > 0.5;
34+
let z = select(a * a, 0.5 * (1.0 - a), big);
35+
let s = select(a, sqrt(z), big);
36+
var p = ((((4.2163199048e-2 * z + 2.4181311049e-2) * z + 4.5470025998e-2) * z + 7.4953002686e-2) * z + 1.6666752422e-1) * z * s + s;
37+
if (big) {
38+
p = 1.5707963267948966 - 2.0 * p;
39+
}
40+
return select(p, -p, x < 0.0);
41+
}
42+
43+
fn acos_p(x: f32) -> f32 {
44+
if (x > 0.5) {
45+
return 2.0 * asin_p(sqrt(0.5 * (1.0 - x)));
46+
}
47+
if (x < -0.5) {
48+
return 3.141592653589793 - 2.0 * asin_p(sqrt(0.5 * (1.0 + x)));
49+
}
50+
return 1.5707963267948966 - asin_p(x);
51+
}
52+
1953
// ---------------------------------------------------------------- sampling (raster::Image)
2054

2155
fn tex_get(t: texture_2d<f32>, x: i32, y: i32) -> vec4<f32> {

‎crates/gpu/src/shaders/fx_distort.wgsl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ fn dst_cclens(@builtin(global_invocation_id) gid: vec3<u32>) {
215215
}
216216
var s = q;
217217
if (d >= 1e-9) {
218-
let g = powz(asin(min(d / r, 1.0)) / DST_HALF_PI, P.f[0].w);
218+
let g = powz(asin_p(min(d / r, 1.0)) / DST_HALF_PI, P.f[0].w);
219219
let k = r * g / d;
220220
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
221221
}

‎crates/gpu/src/shaders/fx_gen2.wgsl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ fn fg2_threads(@builtin(global_invocation_id) gid: vec3<u32>) {
653653
let fv = v / th - f32(cj);
654654
let in_h = abs(fv - 0.5) < coverage * 0.5;
655655
let in_v = abs(fu - 0.5) < coverage * 0.5;
656-
let h_top = ((ci + cj) % (2 * overlaps) + 2 * overlaps) % (2 * overlaps) < overlaps;
656+
let h_top = imod(ci + cj, 2 * overlaps) < overlaps;
657657
var horizontal: bool;
658658
if (in_h && in_v) {
659659
horizontal = h_top;

0 commit comments

Comments
 (0)