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>
92 lines
2.3 KiB
Rust
92 lines
2.3 KiB
Rust
use crate::{UiRegion, util::Id, util::Vec2};
|
|
use wgpu::*;
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
|
pub struct WindowUniform {
|
|
pub dim: Vec2,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct PrimitiveInstance {
|
|
pub region: UiRegion,
|
|
pub mask_idx: MaskIdx,
|
|
pub move_idx: MoveIdx,
|
|
}
|
|
|
|
impl PrimitiveInstance {
|
|
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
|
0 => Float32x2,
|
|
1 => Float32x2,
|
|
2 => Float32x2,
|
|
3 => Float32x2,
|
|
4 => Uint32,
|
|
5 => Uint32,
|
|
];
|
|
|
|
pub fn desc() -> VertexBufferLayout<'static> {
|
|
VertexBufferLayout {
|
|
array_stride: std::mem::size_of::<Self>() as BufferAddress,
|
|
step_mode: VertexStepMode::Instance,
|
|
attributes: &Self::ATTRIBS,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type MaskIdx = Id<u32>;
|
|
|
|
impl MaskIdx {
|
|
pub const NONE: Self = Self::preset(u32::MAX);
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct Mask {
|
|
pub region: UiRegion,
|
|
pub move_idx: MoveIdx,
|
|
}
|
|
|
|
/// Its own type rather than another `Id<u32>`, because it sits beside
|
|
/// `MaskIdx` in an instance and the two must not be swappable.
|
|
#[repr(transparent)]
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
|
|
pub struct MoveIdx(u32);
|
|
|
|
impl MoveIdx {
|
|
pub const NONE: Self = Self(u32::MAX);
|
|
|
|
pub(crate) fn slot(idx: usize) -> Self {
|
|
Self(idx as u32)
|
|
}
|
|
|
|
pub(crate) fn idx(self) -> usize {
|
|
self.0 as usize
|
|
}
|
|
}
|
|
|
|
/// One link of the chain a primitive's position is resolved through: the box
|
|
/// its contents are placed within, given in the coordinates of the slot it
|
|
/// 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)]
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub struct MoveOffset {
|
|
pub region: UiRegion,
|
|
pub parent: MoveIdx,
|
|
}
|
|
|
|
unsafe impl bytemuck::Pod for MoveOffset {}
|
|
unsafe impl bytemuck::Zeroable for MoveOffset {}
|
|
|
|
impl MoveOffset {
|
|
pub fn root(parent: MoveIdx) -> Self {
|
|
Self {
|
|
region: UiRegion::FULL,
|
|
parent,
|
|
}
|
|
}
|
|
}
|