`Px` and `PxVec2` reach the last places a pixel was a float: the window, the box a widget reads, the box it is compared against, and `PixelRegion`. A pointer, a wheel notch and a shaped glyph advance still arrive as floats, and each is put on the grid where it arrives. `Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the `exact`/tolerant split it existed for: `at` is the length a widget read, an open end is the next step along, and `same_px` is equality. `Span`'s margin from `5ed9e87` goes too -- the box a parent hands back and the sum of what its children asked for are counts of the same step, so the boundary decides the same way from either side. Three things had to be true for that, and were not: `Holds::through` inverts `px + rel * box`, which rounds -- so a part of a given length came from a range of boxes, and inverting the length alone gave a point that need not contain the box the part was drawn in. It now maps the half step either side, and one more for a length composed down the chain against the same length measured against the window. `RegionRemap` translates when a box only moved, rather than dividing to find each part's fraction and multiplying to place it again. Two roundings landed a step from where growing the tree that way does; a move is exact on a grid, which is the whole reason `tests/drift.rs` was written. A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length reached two ways was one step, and one step was 0.016 px -- enough to move a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now, and the shader's copy is prepended from them rather than written twice. Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and `random` byte-identical at 1920x1200. What the fuzzers ask for is now a step, not a twentieth of a pixel: the shrinker's five cases agree within one (`resize` exactly), and the oracle's two-operation cases within two. The residue is a single rounding either way -- it scales with the grid rather than accumulating, which is why it is a thousandth of a pixel now. Closing it means one way of asking how long a box is, rather than a chain composed down and a length measured against the window; that is a bigger change than this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
5.6 KiB
WebGPU Shading Language
190 lines
5.6 KiB
WebGPU Shading Language
// Prepended to every primitive's shader, which declares its own instance data
|
|
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
|
// shading one instance of it. What it samples, if anything, is bound at group
|
|
// 2: the texture at binding 0 and the sampler at binding 1.
|
|
|
|
@group(0) @binding(0)
|
|
var<uniform> window: WindowUniform;
|
|
@group(0) @binding(1)
|
|
var<storage> masks: array<Mask>;
|
|
@group(0) @binding(2)
|
|
var<storage> move_offsets: array<MoveOffset>;
|
|
|
|
struct WindowUniform {
|
|
dim: vec2<f32>,
|
|
};
|
|
|
|
struct Mask {
|
|
x: RawSpan,
|
|
y: RawSpan,
|
|
move_idx: u32,
|
|
}
|
|
|
|
struct MoveOffset {
|
|
x: RawSpan,
|
|
y: RawSpan,
|
|
parent: u32,
|
|
}
|
|
|
|
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
|
|
// what it stores is a whole count of each, both powers of two, so decoding
|
|
// is exact and the number here is the number the CPU decided.
|
|
|
|
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
|
// composes to within half a step of a pixel boundary is on that boundary and
|
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
|
return floor(v + PX_STEP * 0.5);
|
|
}
|
|
|
|
struct RawScalar {
|
|
rel: i32,
|
|
px: i32,
|
|
}
|
|
|
|
struct RawSpan {
|
|
start: RawScalar,
|
|
end: RawScalar,
|
|
}
|
|
|
|
fn scalar_of(raw: RawScalar) -> UiScalar {
|
|
return UiScalar(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
|
|
}
|
|
|
|
fn span_of(raw: RawSpan) -> UiSpan {
|
|
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
|
|
}
|
|
|
|
fn scalar_of_pair(raw: vec2<i32>) -> UiScalar {
|
|
return UiScalar(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
|
|
}
|
|
|
|
struct Region {
|
|
x: UiSpan,
|
|
y: UiSpan,
|
|
}
|
|
|
|
const MOVE_NONE: u32 = 4294967295u;
|
|
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
|
|
// rather than any real tree, and the CPU walk uses the same number so both
|
|
// resolve a deep one the same way.
|
|
const CHAIN_LIMIT: u32 = 64u;
|
|
|
|
// The same expression `UiScalar::within` uses, in floats rather than on the
|
|
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
|
// entry instead of walking it. What has to hold is that this agrees with
|
|
// itself frame to frame, not that it matches the CPU to the last bit.
|
|
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
|
return UiScalar(
|
|
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
|
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
|
);
|
|
}
|
|
|
|
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
|
|
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
|
|
}
|
|
|
|
fn resolve_move(idx: u32, local: Region) -> Region {
|
|
var r = local;
|
|
var at = idx;
|
|
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
|
if at == MOVE_NONE {
|
|
break;
|
|
}
|
|
let entry = move_offsets[at];
|
|
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
|
|
at = entry.parent;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
struct UiSpan {
|
|
start: UiScalar,
|
|
end: UiScalar,
|
|
}
|
|
|
|
struct UiScalar {
|
|
rel: f32,
|
|
px: f32,
|
|
}
|
|
|
|
struct InstanceInput {
|
|
@location(0) x_start: vec2<i32>,
|
|
@location(1) x_end: vec2<i32>,
|
|
@location(2) y_start: vec2<i32>,
|
|
@location(3) y_end: vec2<i32>,
|
|
@location(4) mask_idx: u32,
|
|
@location(5) move_idx: u32,
|
|
}
|
|
|
|
struct VertexOutput {
|
|
@location(0) top_left: vec2<f32>,
|
|
@location(1) bot_right: vec2<f32>,
|
|
@location(2) uv: vec2<f32>,
|
|
@location(3) @interpolate(flat) mask_idx: u32,
|
|
@location(4) @interpolate(flat) idx: u32,
|
|
@builtin(position) clip_position: vec4<f32>,
|
|
};
|
|
|
|
@vertex
|
|
fn vs_main(
|
|
@builtin(vertex_index) vi: u32,
|
|
@builtin(instance_index) ii: u32,
|
|
in: InstanceInput,
|
|
) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
|
|
let local = Region(
|
|
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
|
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
|
);
|
|
let r = resolve_move(in.move_idx, local);
|
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
|
let top_left_px = vec2(r.x.start.px, r.y.start.px);
|
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
|
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
|
|
|
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
|
|
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
|
|
let size = bot_right - top_left;
|
|
|
|
let uv = vec2<f32>(
|
|
f32(vi % 2u),
|
|
f32(vi / 2u)
|
|
);
|
|
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
out.uv = uv;
|
|
out.top_left = top_left;
|
|
out.bot_right = bot_right;
|
|
out.mask_idx = in.mask_idx;
|
|
out.idx = ii;
|
|
|
|
return out;
|
|
}
|
|
|
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|
if in.mask_idx == 4294967295u {
|
|
return color;
|
|
}
|
|
let mask = masks[in.mask_idx];
|
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
|
// clips content that moves inside it.
|
|
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y)));
|
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
|
let tl_px = vec2(m.x.start.px, m.y.start.px);
|
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
|
|
|
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
|
|
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
|
|
let pos = in.clip_position.xy;
|
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
|
return color * 0.0;
|
|
}
|
|
return color;
|
|
}
|