Prune commentary and stale Rust port notes

This commit is contained in:
iris committed 2026-09-10 00:44:13 -04:00
1 parent 3ae034a47b
commit 1e6d3b1edd
84 files changed
+334 -5648

No files matched your search

-109
View File
@@ -1,41 +1,3 @@
//! The CPU rounded-rect SDF and the shader's own must agree.
//!
//! LAYOUT.md's "Masks with a shape" turns on it: the fragment stage clips
//! a masked subtree with `shader.wgsl`'s `rounded_rect_coverage`, and the
//! hit test (`UiRenderState::mask_admits`) clips the *same* subtree with
//! `iris_core::rounded_rect_coverage`, so a corner that cannot be tapped
//! and a corner that is not drawn are the same corner only while the two
//! functions answer the same. Nothing else checks that: both sides are
//! individually plausible and drift shows up as a control that is a pixel
//! or two off, which is exactly what nobody notices.
//!
//! So this runs **the real shader text**, lifted out of
//! `iris_core::SHAPE_SHADER` by name rather than copied here, over a grid
//! of points, and compares what came back with the Rust function at the
//! same points. This is the only test in the workspace that needs a GPU;
//! everything else about masks is layer 1 (docs/RUST.md's "Three test
//! layers"). It fails rather than skips when there is no adapter, because
//! a check that quietly did not run reads exactly like a check that
//! passed.
//!
//! **It is a render pass, and it asks for `iris_core::device_limits()`,
//! because those are the two things iris itself does.** The first version
//! of this test was a compute pass, which meant asking for compute limits
//! that `device_limits()` deliberately zeroes -- docs/RUST.md, 2026-09-05:
//! nothing in `iris`/`iris-core` creates a `ComputePipeline` or writes a
//! `@compute` stage, so the limits stopped being requested rather than a
//! fallback being built for a capability nothing uses. A test that needs
//! a capability the thing under test has never needed is testing the
//! wrong device, which is reason enough.
//!
//! It is **not** why that version crashed; see [`vulkan_instance`] for
//! what that crash actually was and why nothing here has to work around
//! it any more.
// `OnceLock<wgpu::Instance>` needs `Instance: Sync`, and wgpu's type
// graph is deep enough that proving it overflows rustc's default trait
// recursion limit of 128. Nothing here is recursive; the limit is a
// compile-time budget, and this is the documented way to raise it.
#![recursion_limit = "256"]
use std::sync::OnceLock;
@@ -44,35 +6,16 @@ use iris_core::{SHAPE_SHADER, rounded_rect_coverage, util::Vec2};
use pollster::FutureExt;
use wgpu::util::DeviceExt;
/// The rect the grid is sampled against, in window pixels. Deliberately
/// off the whole-pixel grid: the shader floors a primitive's corners, but
/// `rounded_rect_coverage` is handed pixels either side of that and has to
/// agree at fractional positions too -- the phone's 2.55 density puts
/// nothing on a whole pixel.
const TOP_LEFT: Vec2 = Vec2::new(10.5, 20.25);
const BOT_RIGHT: Vec2 = Vec2::new(170.75, 90.0);
/// Radii spanning what the widgets actually ask for, plus the two edges of
/// the function's own domain: a square corner, and one large enough that
/// `min(edge, radius)` stops mattering.
const RADII: [f32; 5] = [0.0, 0.75, 8.0, 20.0, 34.0];
/// The grid, as an attachment: one texel per probe point. `GRID_W` is a
/// multiple of 64 so that a row of `R32Float` is 256-byte aligned, which
/// is what `copy_texture_to_buffer` requires; at `STEP` this spans the
/// rect above and about four pixels of margin on every side, so the
/// feather is sampled rather than stepped over.
const GRID_W: u32 = 384;
const GRID_H: u32 = 192;
const STEP: f32 = 0.5;
const ORIGIN: Vec2 = Vec2::new(TOP_LEFT.x - 4.0, TOP_LEFT.y - 4.0);
/// f32 arithmetic in two compilers, not one: `length`/`sqrt` and
/// `smoothstep` are each allowed a unit or two in the last place, and the
/// GPU may contract a multiply-add the CPU does not. A coverage is in
/// [0, 1], so this is about six decimal digits -- four orders of magnitude
/// tighter than the half-pixel feather the hit test reads, which is what
/// the agreement is actually for.
const TOLERANCE: f32 = 1e-5;
#[test]
@@ -114,7 +57,6 @@ fn mask_sdf_matches_the_shader() {
corner stops being tappable where it is drawn.",
);
// The half that would pass on a function returning a constant.
assert!(
inside > 0 && feather > 0 && outside > 0,
"the grid never crossed an edge ({inside} in, {feather} on the feather, {outside} out), \
@@ -122,18 +64,10 @@ fn mask_sdf_matches_the_shader() {
);
}
/// The probe position of texel `(x, y)` -- the one place the mapping
/// lives, so the CPU side and the fragment stage cannot walk different
/// grids.
fn probe_at(x: u32, y: u32) -> Vec2 {
Vec2::new(ORIGIN.x + x as f32 * STEP, ORIGIN.y + y as f32 * STEP)
}
/// `shader.wgsl`'s own `rounded_rect_coverage`, evaluated at every texel
/// of an `R32Float` attachment: one fragment per grid point, read back
/// whole. A fragment stage because that is the stage the function is
/// really called from, so what this compares is the code path that draws
/// rather than a second one built to be measurable.
fn run_shader(gpu: &Gpu, radius: f32) -> Vec<f32> {
let Gpu { device, queue, .. } = gpu;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -141,10 +75,6 @@ fn run_shader(gpu: &Gpu, radius: f32) -> Vec<f32> {
source: wgpu::ShaderSource::Wgsl(probe_source().into()),
});
// R32Float, not an 8-bit colour format: a coverage quantised to 1/255
// could not be compared against the CPU's at anything like TOLERANCE,
// and the comparison would then be measuring the texture rather than
// the two functions.
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("mask sdf coverage"),
size: wgpu::Extent3d {
@@ -205,9 +135,6 @@ fn run_shader(gpu: &Gpu, radius: f32) -> Vec<f32> {
}],
});
// `copy_texture_to_buffer` wants each row 256-byte aligned; GRID_W is
// chosen so that it already is, rather than padding and unpicking the
// padding on the way out.
let row_bytes = GRID_W * 4;
assert_eq!(row_bytes % 256, 0, "GRID_W must keep rows 256-byte aligned");
let out_size = u64::from(row_bytes) * u64::from(GRID_H);
@@ -272,42 +199,17 @@ fn run_shader(gpu: &Gpu, radius: f32) -> Vec<f32> {
coverage
}
/// One `wgpu::Instance` for the process, created on first use and never
/// destroyed.
///
/// **Why it is a static rather than a value the test owns.** Destroying
/// the last `VkInstance` makes the Vulkan loader `dlclose` the ICD, and
/// Mesa's ICD here registers a `pthread_key_create` destructor pointing
/// into its own text without being linked `-z nodelete`. glibc then calls
/// that destructor when the thread exits -- through an address that is no
/// longer mapped. libtest runs every `#[test]` on a spawned thread, so a
/// test that opens and closes an instance segfaults *after* printing its
/// result, which reads exactly like the test failing. Measured
/// 2026-09-08 with `scripts/rigs/gpu-probe`'s `teardown` bin: it
/// needs no wgpu (raw `ash` does it too), no GPU work, and no device --
/// an instance created and destroyed on a spawned thread is enough, and
/// keeping any one instance alive is enough to prevent it.
///
/// Devices, queues and everything else drop normally; only the instance
/// is held, which is what wgpu asks for anyway (one instance per
/// process). So this costs one instance for the length of a test binary
/// and buys ordinary drops everywhere else.
fn vulkan_instance() -> &'static wgpu::Instance {
static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
INSTANCE.get_or_init(wgpu::Instance::default)
}
/// The device this test draws with.
struct Gpu {
device: wgpu::Device,
queue: wgpu::Queue,
}
impl Gpu {
/// Opens the device this test draws with, and reports which adapter
/// answered, because that is not a detail here: a run on llvmpipe and
/// a run on the host's GPU are otherwise indistinguishable in the
/// log, and only one of them is a check of what the phone will do.
fn open() -> Self {
let instance = vulkan_instance();
let adapter = instance
@@ -326,7 +228,6 @@ impl Gpu {
);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
// What iris itself asks for -- see this file's header.
required_limits: iris_core::device_limits(),
..Default::default()
})
@@ -336,8 +237,6 @@ impl Gpu {
}
}
/// What the fragment stage needs to turn its own texel into a probe
/// position: the rect being sampled, and where texel (0, 0) sits.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Probe {
@@ -349,11 +248,6 @@ struct Probe {
_pad: [f32; 3],
}
/// The probe module: the two functions **lifted from `shader.wgsl`
/// itself**, plus an entry point that calls the outer one. Lifted rather
/// than copied so there is nothing to keep in step -- an edit to the
/// shader is what this test is for, and a copy here would be edited along
/// with it.
fn probe_source() -> String {
format!(
"{}\n{}\n\
@@ -380,9 +274,6 @@ fn probe_source() -> String {
)
}
/// One WGSL function's whole text, from its `fn` keyword to the `}` that
/// closes its body, found by matching braces. Panics by name when the
/// function is not there, which is what a rename looks like from here.
fn wgsl_fn(name: &str) -> &'static str {
let start = SHAPE_SHADER
.find(&format!("fn {name}("))