Carry a box in a move slot, not a translation
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>
This commit is contained in:
1 parent
db1751fdfd
commit
1f9dc48b80
4 files changed
+69
-48
No files matched your search
@@ -65,13 +65,16 @@ impl MoveIdx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One link of the chain a primitive's position is resolved through: a
|
/// One link of the chain a primitive's position is resolved through: the box
|
||||||
/// translation in physical pixels, and the slot it is relative to. Moving a
|
/// its contents are placed within, given in the coordinates of the slot it
|
||||||
/// subtree writes its own slot and nothing else.
|
/// names. Moving or resizing a subtree writes its own slot and nothing else.
|
||||||
|
///
|
||||||
|
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
|
||||||
|
/// extent, which collapses everything under it to a point.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct MoveOffset {
|
pub struct MoveOffset {
|
||||||
pub delta: Vec2,
|
pub region: UiRegion,
|
||||||
pub parent: MoveIdx,
|
pub parent: MoveIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ unsafe impl bytemuck::Zeroable for MoveOffset {}
|
|||||||
impl MoveOffset {
|
impl MoveOffset {
|
||||||
pub fn root(parent: MoveIdx) -> Self {
|
pub fn root(parent: MoveIdx) -> Self {
|
||||||
Self {
|
Self {
|
||||||
delta: Vec2::ZERO,
|
region: UiRegion::FULL,
|
||||||
parent,
|
parent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,28 +21,45 @@ struct Mask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct MoveOffset {
|
struct MoveOffset {
|
||||||
delta: vec2<f32>,
|
x: UiSpan,
|
||||||
|
y: UiSpan,
|
||||||
parent: u32,
|
parent: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Region {
|
||||||
|
x: UiSpan,
|
||||||
|
y: UiSpan,
|
||||||
|
}
|
||||||
|
|
||||||
const MOVE_NONE: u32 = 4294967295u;
|
const MOVE_NONE: u32 = 4294967295u;
|
||||||
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
|
// 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
|
// rather than any real tree, and the CPU walk uses the same number so both
|
||||||
// resolve a deep one the same way.
|
// resolve a deep one the same way.
|
||||||
const CHAIN_LIMIT: u32 = 64u;
|
const CHAIN_LIMIT: u32 = 64u;
|
||||||
|
|
||||||
fn resolve_move(idx: u32) -> vec2<f32> {
|
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
||||||
var total = vec2<f32>(0.0, 0.0);
|
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;
|
var at = idx;
|
||||||
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
||||||
if at == MOVE_NONE {
|
if at == MOVE_NONE {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let entry = move_offsets[at];
|
let entry = move_offsets[at];
|
||||||
total += entry.delta;
|
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
|
||||||
at = entry.parent;
|
at = entry.parent;
|
||||||
}
|
}
|
||||||
return total;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiSpan {
|
struct UiSpan {
|
||||||
@@ -81,14 +98,18 @@ fn vs_main(
|
|||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
let local = Region(
|
||||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
||||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
||||||
let bot_right_abs = vec2(in.x_end.y, 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 moved = resolve_move(in.move_idx);
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs + moved);
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs + moved);
|
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -111,16 +132,16 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
let mask = masks[in.mask_idx];
|
let mask = masks[in.mask_idx];
|
||||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
|
||||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
|
||||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
|
||||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
|
||||||
|
|
||||||
// Its own chain, not the drawn primitive's, so a stationary viewport
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||||
// clips content that moves inside it.
|
// clips content that moves inside it.
|
||||||
let moved = resolve_move(mask.move_idx);
|
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y));
|
||||||
let top_left = floor(tl * window.dim) + floor(tl_abs + moved);
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
||||||
let bot_right = floor(br * window.dim) + floor(br_abs + moved);
|
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;
|
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 {
|
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 * 0.0;
|
||||||
|
|||||||
+15
-14
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId,
|
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
||||||
Widgets,
|
WidgetId, Widgets,
|
||||||
util::{Arena, Id, TrackedArena, Vec2},
|
util::{Arena, Id, TrackedArena},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How far the shader will walk a move chain. It bounds a malformed cycle
|
/// How far the shader will walk a move chain. It bounds a malformed cycle
|
||||||
@@ -46,26 +46,27 @@ impl Moves {
|
|||||||
self.arena.remove(Id::preset(idx.idx() as u32));
|
self.arena.remove(Id::preset(idx.idx() as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a slot's translation, in physical pixels, relative to its parent.
|
/// Sets the box a slot's contents are placed within, itself given in the
|
||||||
pub fn set(&mut self, idx: MoveIdx, delta: Vec2) {
|
/// coordinates of its parent slot.
|
||||||
|
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
|
||||||
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
if entry.delta != delta {
|
if entry.region != region {
|
||||||
entry.delta = delta;
|
entry.region = region;
|
||||||
self.changed = true;
|
self.changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translation a primitive in `idx` has accumulated, which is the
|
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
||||||
/// same walk the vertex shader does.
|
/// the same walk the vertex shader does.
|
||||||
pub fn resolve(&self, idx: MoveIdx) -> Vec2 {
|
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||||
let mut total = Vec2::ZERO;
|
let mut region = local;
|
||||||
let mut at = idx;
|
let mut at = idx;
|
||||||
for _ in 0..CHAIN_LIMIT {
|
for _ in 0..CHAIN_LIMIT {
|
||||||
if at == MoveIdx::NONE {
|
if at == MoveIdx::NONE {
|
||||||
return total;
|
return region;
|
||||||
}
|
}
|
||||||
let entry = self.arena[at.idx()];
|
let entry = self.arena[at.idx()];
|
||||||
total += entry.delta;
|
region = region.within(&entry.region);
|
||||||
at = entry.parent;
|
at = entry.parent;
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -73,7 +74,7 @@ impl Moves {
|
|||||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||||
and the shader stops at the same depth"
|
and the shader stops at the same depth"
|
||||||
);
|
);
|
||||||
total
|
region
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> &[MoveOffset] {
|
pub fn entries(&self) -> &[MoveOffset] {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ impl UiRenderState {
|
|||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
let move_idx = self.move_slot(id, parent);
|
let move_idx = self.move_slot(id, parent);
|
||||||
self.moves.set(move_idx, Vec2::ZERO);
|
self.moves.set(move_idx, UiRegion::FULL);
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ impl UiRenderState {
|
|||||||
// coordinates its parent drew, so the chain above applies alike.
|
// coordinates its parent drew, so the chain above applies alike.
|
||||||
let moved =
|
let moved =
|
||||||
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
|
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
|
||||||
self.moves.set(slot, moved);
|
self.moves.set(slot, UiRegion::FULL.offset(moved));
|
||||||
return Some(size);
|
return Some(size);
|
||||||
}
|
}
|
||||||
if !self.reusable(id, region, rsc) || !old.stretchable() {
|
if !self.reusable(id, region, rsc) || !old.stretchable() {
|
||||||
@@ -269,7 +269,7 @@ impl UiRenderState {
|
|||||||
*region = region.stretch(&from, &to);
|
*region = region.stretch(&from, &to);
|
||||||
}
|
}
|
||||||
active.region = active.region.stretch(&from, &to);
|
active.region = active.region.stretch(&from, &to);
|
||||||
self.moves.set(active.move_idx, Vec2::ZERO);
|
self.moves.set(active.move_idx, UiRegion::FULL);
|
||||||
// SAFETY: children cannot be recursive
|
// SAFETY: children cannot be recursive
|
||||||
let children = unsafe { forget_ref(&active.children) };
|
let children = unsafe { forget_ref(&active.children) };
|
||||||
for child in children {
|
for child in children {
|
||||||
@@ -367,12 +367,8 @@ impl UiRenderState {
|
|||||||
/// chain above has moved since, which is the walk the vertex shader does.
|
/// chain above has moved since, which is the walk the vertex shader does.
|
||||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
let active = self.active.get(&id.id())?;
|
let active = self.active.get(&id.id())?;
|
||||||
let moved = self.moves.resolve(active.move_idx);
|
let region = self.moves.resolve(active.move_idx, active.region);
|
||||||
let region = active.region.to_px(self.output_size);
|
Some(region.to_px(self.output_size))
|
||||||
Some(PixelRegion {
|
|
||||||
top_left: region.top_left + moved,
|
|
||||||
bot_right: region.bot_right + moved,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// redraws a widget that's currently active (drawn)
|
/// redraws a widget that's currently active (drawn)
|
||||||
|
|||||||
Reference in new issue
Block a user