A slot now holds the box its contents are placed within, in the coordinates of the slot it names, and `prelude.wgsl` composes the chain with `within` instead of adding a delta. A translation is the special case where the box has its parent's relative extent, so every caller passes `UiRegion::FULL.offset(delta)` and nothing changes on screen yet: 42 tests pass and `tabs` at 1920x1200 is byte-identical. `Moves::resolve` takes the region to compose rather than returning a sum, so the CPU walk is the same operation the shader performs. Measured against the translate slot on the same binary with `tests/chain_cost.rs`, 200k instances: +0.6% at depth 1, +0.5% at 2, +0.8% at 4, then +9.6% at 8 and +32.2% at 64. Free at the depth opt-in slots produce, which is the next commit; the per-level cost was always the dependent load rather than the arithmetic. The identity is `UiRegion::FULL` rather than zero, which `MoveOffset`'s comment says beside the `Zeroable` that `Pod` requires: a zeroed entry is a box of no extent and collapses its subtree to a point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
4.2 KiB
WebGPU Shading Language
151 lines
4.2 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: UiSpan,
|
|
y: UiSpan,
|
|
move_idx: u32,
|
|
}
|
|
|
|
struct MoveOffset {
|
|
x: UiSpan,
|
|
y: UiSpan,
|
|
parent: u32,
|
|
}
|
|
|
|
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;
|
|
|
|
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
|
return UiScalar(
|
|
mix(p.start.rel, p.end.rel, s.rel),
|
|
s.abs + mix(p.start.abs, p.end.abs, 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, entry.x), span_within(r.y, entry.y));
|
|
at = entry.parent;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
struct UiSpan {
|
|
start: UiScalar,
|
|
end: UiScalar,
|
|
}
|
|
|
|
struct UiScalar {
|
|
rel: f32,
|
|
abs: f32,
|
|
}
|
|
|
|
struct InstanceInput {
|
|
@location(0) x_start: vec2<f32>,
|
|
@location(1) x_end: vec2<f32>,
|
|
@location(2) y_start: vec2<f32>,
|
|
@location(3) y_end: vec2<f32>,
|
|
@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(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
|
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
|
);
|
|
let r = resolve_move(in.move_idx, local);
|
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
|
let top_left_abs = vec2(r.x.start.abs, r.y.start.abs);
|
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
|
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs);
|
|
|
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
|
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(mask.x, mask.y));
|
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
|
let tl_abs = vec2(m.x.start.abs, m.y.start.abs);
|
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
|
let br_abs = vec2(m.x.end.abs, m.y.end.abs);
|
|
|
|
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
|
let bot_right = floor(br * window.dim) + floor(br_abs);
|
|
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;
|
|
}
|