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>
194 lines
5.7 KiB
WebGPU Shading Language
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;
|
|
}
|