From 80565e095bfae34cf1f7d1a6b4b07ec0b24b2b08 Mon Sep 17 00:00:00 2001 From: allan sargeant <8385907+stoatworks-labs@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:08:39 +0100 Subject: [PATCH] Make the release script's own gate pass again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/release-local.sh` runs `cargo clippy --release --all-targets -D warnings` before it builds anything, and that gate has been failing: the glam 0.33 bump deprecated `Mat4::look_at_rh` and `Mat4::perspective_rh`, and clippy learned `chunks_exact_to_as_chunks`. Neither is a bug, and together they meant there was no way to build a local .app at all. The glam replacement needs saying out loud. 0.33 offers one perspective per clip space, named after the API that popularised each, and all three are right-handed: `vulkan` has wgpu's depth range with Y *down*, `opengl` puts Z in -1..1, and only `directx` is Z in 0..1 with Y up — what wgpu, and so Metal here, actually wants. Pick either of the others and the stage still renders, upside down or clipped at half its depth. The new matrices were checked equal to the old ones before the deprecated calls went, and a test now pins the clip space directly rather than trusting the module name. The About window is vendored from stoatworks-backend and says so in its header, so its one clippy nit is silenced at the `mod` instead of edited in a copy. Worth fixing upstream and re-syncing. Co-Authored-By: Claude Opus 5 --- crates/unmapper-core/src/stage.rs | 49 ++++++++++++++++++++++++-- crates/unmapper-gui/src/main.rs | 4 +++ crates/unmapper-render/src/model.rs | 2 +- crates/unmapper-render/src/pattern.rs | 2 +- crates/unmapper-render/tests/render.rs | 8 ++--- 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/crates/unmapper-core/src/stage.rs b/crates/unmapper-core/src/stage.rs index 54634a3..80016a6 100644 --- a/crates/unmapper-core/src/stage.rs +++ b/crates/unmapper-core/src/stage.rs @@ -455,11 +455,25 @@ impl Default for Camera { impl Camera { pub fn view_matrix(&self) -> glam::Mat4 { - glam::Mat4::look_at_rh(self.position, self.target, self.up) + glam::camera::rh::view::look_at_mat4(self.position, self.target, self.up) } + /// # Why `directx`, on a Mac, in a Vulkan-shaped API + /// + /// glam 0.33 replaced `Mat4::perspective_rh` with one function per clip + /// space, named after the graphics API that popularised each. The one wgpu + /// wants — and therefore Metal, here — is **Z in `0..1` with Y up**, which + /// is glam's `directx` module. `vulkan` is the same depth range with Y + /// *down*, and would hand back a stage that is upside down but otherwise + /// entirely plausible. Verified equal to the old `perspective_rh` before the + /// deprecated call was removed. pub fn projection_matrix(&self, aspect: f32) -> glam::Mat4 { - glam::Mat4::perspective_rh(self.fov_y_deg.to_radians(), aspect, self.near, self.far) + glam::camera::rh::proj::directx::perspective( + self.fov_y_deg.to_radians(), + aspect, + self.near, + self.far, + ) } pub fn view_projection(&self, aspect: f32) -> glam::Mat4 { @@ -830,6 +844,37 @@ mod tests { } } + #[test] + fn the_projection_is_wgpus_clip_space_and_not_one_of_its_neighbours() { + // glam 0.33 replaced `Mat4::perspective_rh` with one function per clip + // space. All three are right-handed and none of them is obviously wrong + // at a glance: `vulkan` has the same depth range with Y *down*, so the + // stage comes out upside down; `opengl` puts Z in -1..1, so half the + // depth range is clipped. Both still render a picture. This pins the one + // wgpu actually wants. + let cam = Camera::default(); + let vp = cam.view_projection(16.0 / 9.0); + let ndc = |p: Vec3| { + let clip = vp * p.extend(1.0); + clip.truncate() / clip.w + }; + + let forward = cam.forward(); + let near = ndc(cam.position + forward * cam.near); + let far = ndc(cam.position + forward * cam.far); + assert!(near.z.abs() < 1e-3, "near plane should be depth 0, got {near:?}"); + assert!( + (far.z - 1.0).abs() < 1e-3, + "far plane should be depth 1, got {far:?}" + ); + + let above = ndc(cam.target + Vec3::Y); + assert!( + above.y > 0.0, + "up should be +Y in clip space, got {above:?}" + ); + } + #[test] fn projecting_and_unprojecting_are_the_same_map_read_both_ways() { // What makes a dragged handle stay under the pointer: pick a point, and diff --git a/crates/unmapper-gui/src/main.rs b/crates/unmapper-gui/src/main.rs index 3a6a9b0..5ea6a10 100644 --- a/crates/unmapper-gui/src/main.rs +++ b/crates/unmapper-gui/src/main.rs @@ -13,6 +13,10 @@ //! to interleave with UnMapper's render passes. mod about_data; +// Vendored from stoatworks-backend/about/rust, which is the master: the copy +// here must not be edited, so its one clippy nit is silenced at the import +// instead. Fix it there and re-run sync-about.py, and this can go. +#[allow(clippy::redundant_closure)] mod about_window; mod outputs; mod state; diff --git a/crates/unmapper-render/src/model.rs b/crates/unmapper-render/src/model.rs index 1c8b347..b32dfc6 100644 --- a/crates/unmapper-render/src/model.rs +++ b/crates/unmapper-render/src/model.rs @@ -156,7 +156,7 @@ fn append_primitive( // exported without normals still shades rather than rendering flat black. let had_normals = reader.read_normals().is_some(); if !had_normals { - for tri in indices.chunks_exact(3) { + for tri in indices.as_chunks::<3>().0 { let (a, b, c) = ( base as usize + tri[0] as usize, base as usize + tri[1] as usize, diff --git a/crates/unmapper-render/src/pattern.rs b/crates/unmapper-render/src/pattern.rs index 3d7e661..8e2abd2 100644 --- a/crates/unmapper-render/src/pattern.rs +++ b/crates/unmapper-render/src/pattern.rs @@ -91,7 +91,7 @@ mod tests { fn it_is_fully_opaque_and_the_right_size() { let d = test_pattern(Size::new(64, 32)); assert_eq!(d.len(), 64 * 32 * 4); - assert!(d.chunks_exact(4).all(|p| p[3] == 255)); + assert!(d.as_chunks::<4>().0.iter().all(|p| p[3] == 255)); } #[test] diff --git a/crates/unmapper-render/tests/render.rs b/crates/unmapper-render/tests/render.rs index 00e5895..41254de 100644 --- a/crates/unmapper-render/tests/render.rs +++ b/crates/unmapper-render/tests/render.rs @@ -241,7 +241,7 @@ fn a_bgra_source_renders_the_same_colours_as_an_rgba_one() { // The same image as quad_source(), byte-swapped into BGRA. let mut bgra = quad_source(); - for px in bgra.chunks_exact_mut(4) { + for px in bgra.as_chunks_mut::<4>().0 { px.swap(0, 2); } textures.upload( @@ -974,7 +974,7 @@ mod model { let data = target.read_rgba(&gpu); // Somewhere in the frame there must be grey set geometry... - let has_set = data.chunks_exact(4).any(|p| { + let has_set = data.as_chunks::<4>().0.iter().any(|p| { let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32); r > 20 && (r - g).abs() < 30 && (g - b).abs() < 30 && r < 200 }); @@ -982,7 +982,7 @@ mod model { // ...and red panel, which the model must not have painted over. let has_panel = data - .chunks_exact(4) + .as_chunks::<4>().0.iter() .any(|p| p[0] > 120 && p[1] < 60 && p[2] < 60); assert!( has_panel, @@ -1346,7 +1346,7 @@ fn a_curved_panel_bends_in_previz_and_a_flat_one_does_not() { ); let data = target.read_rgba(&gpu); let lit = data - .chunks_exact(4) + .as_chunks::<4>().0.iter() .filter(|p| p[0] > 40 || p[1] > 40 || p[2] > 40) .count(); (lit, scene.vertices.len())