Files
iris/core/src/render/shader/prelude.wgsl
T
iris-aiandClaude Fable 5.1 5b7800264d Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:00:15 -04:00

194 lines
5.7 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.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
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) -> Len {
return Len(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>) -> Len {
return Len(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 `Len::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: Len, p: UiSpan) -> Len {
return Len(
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: Len,
end: Len,
}
struct Len {
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 + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim + 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 + tl_px);
let bot_right = snap_floor(br * window.dim + 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;
}