diff --git a/core/src/render/data.rs b/core/src/render/data.rs index 032539f..d5b53a6 100644 --- a/core/src/render/data.rs +++ b/core/src/render/data.rs @@ -1,11 +1,10 @@ -use crate::{UiRegion, util::Id}; +use crate::{UiRegion, util::Id, util::Vec2}; use wgpu::*; #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)] pub struct WindowUniform { - pub width: f32, - pub height: f32, + pub dim: Vec2, } #[repr(C)] @@ -13,15 +12,17 @@ pub struct WindowUniform { pub struct PrimitiveInstance { pub region: UiRegion, pub mask_idx: MaskIdx, + pub move_idx: MoveIdx, } impl PrimitiveInstance { - const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![ + const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ 0 => Float32x2, 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Uint32, + 5 => Uint32, ]; pub fn desc() -> VertexBufferLayout<'static> { @@ -43,4 +44,45 @@ impl MaskIdx { #[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`, 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: a +/// translation in physical pixels, and the slot it is relative to. Moving a +/// subtree writes its own slot and nothing else. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct MoveOffset { + pub delta: Vec2, + pub parent: MoveIdx, +} + +unsafe impl bytemuck::Pod for MoveOffset {} +unsafe impl bytemuck::Zeroable for MoveOffset {} + +impl MoveOffset { + pub fn root(parent: MoveIdx) -> Self { + Self { + delta: Vec2::ZERO, + parent, + } + } } diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index c9e2096..d898d0f 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -17,7 +17,7 @@ mod texture; mod util; pub use atlas::*; -pub use data::{Mask, MaskIdx}; +pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset}; pub use primitive::*; const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); @@ -34,6 +34,7 @@ pub struct UiRenderNode { active: Vec, window_buffer: Buffer, masks: ArrBuf, + moves: ArrBuf, } struct RenderLayer { @@ -127,32 +128,35 @@ impl UiRenderNode { for primitive in &mut self.primitives { primitive.render.update(ui); } + let mut regroup = false; if ui.masks.changed { ui.masks.changed = false; - if self.masks.update(device, queue, &ui.masks[..]) { - self.shared_group = Self::shared_group( - device, - &self.shared_layout, - &self.window_buffer, - &self.masks, - ); - } + regroup |= self.masks.update(device, queue, &ui.masks[..]); + } + if ui.moves.changed { + ui.moves.changed = false; + regroup |= self.moves.update(device, queue, ui.moves.entries()); + } + if regroup { + self.shared_group = Self::shared_group( + device, + &self.shared_layout, + &self.window_buffer, + &self.masks, + &self.moves, + ); } } pub fn resize(&mut self, size: impl Into, queue: &Queue) { let size = size.into(); - let slice = &[WindowUniform { - width: size.x, - height: size.y, - }]; + let slice = &[WindowUniform { dim: size }]; queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); } pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { let window_uniform = WindowUniform { - width: config.width as f32, - height: config.height as f32, + dim: Vec2::new(config.width as f32, config.height as f32), }; let window_buffer = device.create_buffer_init(&BufferInitDescriptor { label: Some("window"), @@ -166,7 +170,13 @@ impl UiRenderNode { BufferUsages::STORAGE | BufferUsages::COPY_DST, "ui masks", ); - let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks); + let moves = ArrBuf::new( + device, + BufferUsages::STORAGE | BufferUsages::COPY_DST, + "ui move offsets", + ); + let shared_group = + Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves); Self { shared_layout, @@ -177,6 +187,7 @@ impl UiRenderNode { layers: HashMap::default(), active: Vec::new(), masks, + moves, } } @@ -252,7 +263,8 @@ impl UiRenderNode { }) } - /// What every draw in the ui is given: the window and the masks. + /// What every draw in the ui is given: the window, the masks and the + /// move chain every position is resolved through. fn shared_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ @@ -276,6 +288,16 @@ impl UiRenderNode { }, count: None, }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: BufferSize::new(size_of::() as u64), + }, + count: None, + }, ], label: Some("ui shared"), }) @@ -286,6 +308,7 @@ impl UiRenderNode { layout: &BindGroupLayout, window: &Buffer, masks: &ArrBuf, + moves: &ArrBuf, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, @@ -298,6 +321,10 @@ impl UiRenderNode { binding: 1, resource: masks.buffer.as_entire_binding(), }, + BindGroupEntry { + binding: 2, + resource: moves.buffer.as_entire_binding(), + }, ], label: Some("ui shared"), }) diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index a4b483d..de9a897 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData}; use crate::{ Color, TextureHandle, UiData, UiRegion, WidgetId, render::{ - data::{MaskIdx, PrimitiveInstance}, + data::{MaskIdx, MoveIdx, PrimitiveInstance}, page::GlyphRender, texture::ImageRender, }, @@ -246,6 +246,7 @@ impl LayerDraws { primitive, region, mask_idx, + move_idx, }: PrimitiveInst

, ) -> PrimitiveHandle { self.updated = true; @@ -258,7 +259,11 @@ impl LayerDraws { .get_or_insert_with(InstanceList::new::

) .push( id, - PrimitiveInstance { region, mask_idx }, + PrimitiveInstance { + region, + mask_idx, + move_idx, + }, bytemuck::bytes_of(&primitive), ); PrimitiveHandle { @@ -304,6 +309,7 @@ pub struct PrimitiveInst

{ pub primitive: P, pub region: UiRegion, pub mask_idx: MaskIdx, + pub move_idx: MoveIdx, } pub struct PrimitiveChange { @@ -347,7 +353,7 @@ impl RectPrimitive { /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. -#[repr(C, align(8))] +#[repr(C)] #[derive(Debug, Copy, Clone)] pub struct GlyphPrimitive { pub uv_min: Vec2, @@ -358,8 +364,8 @@ pub struct GlyphPrimitive { pub flags: u32, } -// Manual rather than derived: the align(8) leaves four bytes of padding, which -// is how WGSL lays the struct out. +// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding +// here, which is how WGSL lays the struct out. unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {} impl Primitive for GlyphPrimitive { diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 02c47e0..3fc9cf2 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -7,6 +7,8 @@ var window: WindowUniform; @group(0) @binding(1) var masks: array; +@group(0) @binding(2) +var move_offsets: array; struct WindowUniform { dim: vec2, @@ -15,6 +17,32 @@ struct WindowUniform { struct Mask { x: UiSpan, y: UiSpan, + move_idx: u32, +} + +struct MoveOffset { + delta: vec2, + parent: u32, +} + +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 resolve_move(idx: u32) -> vec2 { + var total = vec2(0.0, 0.0); + var at = idx; + for (var step = 0u; step < CHAIN_LIMIT; step++) { + if at == MOVE_NONE { + break; + } + let entry = move_offsets[at]; + total += entry.delta; + at = entry.parent; + } + return total; } struct UiSpan { @@ -33,6 +61,7 @@ struct InstanceInput { @location(2) y_start: vec2, @location(3) y_end: vec2, @location(4) mask_idx: u32, + @location(5) move_idx: u32, } struct VertexOutput { @@ -57,8 +86,9 @@ fn vs_main( let bot_right_rel = vec2(in.x_end.x, in.y_end.x); let bot_right_abs = vec2(in.x_end.y, in.y_end.y); - 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 moved = resolve_move(in.move_idx); + 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 + moved); let size = bot_right - top_left; let uv = vec2( @@ -86,8 +116,11 @@ fn masked(in: VertexOutput, color: vec4) -> vec4 { let br = vec2(mask.x.end.rel, mask.y.end.rel); let br_abs = vec2(mask.x.end.abs, mask.y.end.abs); - let top_left = floor(tl * window.dim) + floor(tl_abs); - let bot_right = floor(br * window.dim) + floor(br_abs); + // Its own chain, not the drawn primitive's, so a stationary viewport + // clips content that moves inside it. + let moved = resolve_move(mask.move_idx); + let top_left = floor(tl * window.dim) + floor(tl_abs + moved); + let bot_right = floor(br * window.dim) + floor(br_abs + moved); 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; diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index daa6084..6af43f3 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,4 +1,4 @@ -use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId}; +use crate::{LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId}; /// important non rendering data for retained drawing #[derive(Debug)] @@ -15,6 +15,8 @@ pub struct ActiveData { pub size_deps: Vec, /// Whether it read the output's size, and so is wrong when that changes. pub reads_output: bool, + /// The move slot its primitives are positioned through. + pub move_idx: MoveIdx, pub mask: MaskIdx, pub layer: LayerId, } diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 5593164..69b3503 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -1,7 +1,14 @@ use crate::{ - Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena, + Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, + Widgets, + util::{Arena, Id, TrackedArena, Vec2}, }; +/// How far the shader will walk a move chain. It bounds a malformed cycle +/// rather than any real tree; `Moves::resolve` uses the same number so the +/// two agree on what a deep tree resolves to. +pub const CHAIN_LIMIT: u32 = 64; + mod active; mod painter; mod render_state; @@ -18,6 +25,62 @@ pub struct UiData { pub textures: Textures, pub text: TextData, pub masks: TrackedArena, + /// Where each widget's drawing sits relative to its parent's slot, so + /// moving a subtree writes one entry rather than every descendant's + /// primitives. + pub moves: Moves, +} + +#[derive(Default)] +pub struct Moves { + arena: Arena, + pub changed: bool, +} + +impl Moves { + pub fn push(&mut self, parent: MoveIdx) -> MoveIdx { + self.changed = true; + MoveIdx::slot(self.arena.push(MoveOffset::root(parent)).idx()) + } + + pub fn remove(&mut self, idx: MoveIdx) { + self.changed = true; + self.arena.remove(Id::preset(idx.idx() as u32)); + } + + /// Sets a slot's translation, in physical pixels, relative to its parent. + pub fn set(&mut self, idx: MoveIdx, delta: Vec2) { + let entry = self.arena.get_mut(Id::preset(idx.idx() as u32)); + if entry.delta != delta { + entry.delta = delta; + self.changed = true; + } + } + + /// The translation a primitive in `idx` has accumulated, which is the + /// same walk the vertex shader does. + pub fn resolve(&self, idx: MoveIdx) -> Vec2 { + let mut total = Vec2::ZERO; + let mut at = idx; + for _ in 0..CHAIN_LIMIT { + if at == MoveIdx::NONE { + break; + } + let entry = self.arena[at.idx()]; + total += entry.delta; + at = entry.parent; + } + total + } + + pub fn entries(&self) -> &[MoveOffset] { + &self.arena + } + + pub fn clear(&mut self) { + self.changed = true; + self.arena = Arena::default(); + } } pub trait UiRsc { diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 8d229c8..d2258fe 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -2,8 +2,8 @@ use crate::{ Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, render::{ - GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, - TexturePrimitive, + GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, + PrimitiveKind, TexturePrimitive, }, util::Vec2, }; @@ -21,6 +21,8 @@ pub struct Painter<'a> { /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, pub(super) reads_output: bool, + /// The move slot this widget's primitives are positioned through. + pub(super) move_idx: MoveIdx, pub layer: usize, pub(super) id: WidgetId, } @@ -41,6 +43,7 @@ impl<'a> Painter<'a> { primitive, region, mask_idx: self.mask, + move_idx: self.move_idx, }, ); self.push_primitive(h); @@ -67,7 +70,10 @@ impl<'a> Painter<'a> { pub fn set_mask(&mut self, region: UiRegion) { assert!(self.mask == MaskIdx::NONE); - self.mask = self.rsc.ui_mut().masks.push(Mask { region }); + self.mask = self.rsc.ui_mut().masks.push(Mask { + region, + move_idx: self.move_idx, + }); } /// Draws a widget within this widget's region. diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index e40a937..cd2333b 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,6 +1,6 @@ use crate::{ - ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size, - StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, + ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, OnResize, Painter, PixelRegion, Remap, + Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, util::{HashMap, HashSet, Vec2, forget_ref}, }; @@ -12,6 +12,9 @@ pub struct UiRenderState { old_root: Option, resized: bool, draw_started: HashSet, + /// A widget's move slot, which outlives any one `ActiveData`: a redraw + /// replaces that while its children go on pointing at the slot. + moves: HashMap, } impl UiRenderState { @@ -23,6 +26,7 @@ impl UiRenderState { old_root: None, resized: false, draw_started: Default::default(), + moves: Default::default(), } } @@ -103,6 +107,7 @@ impl UiRenderState { } // draw widget + let move_idx = self.move_slot(id, parent, rsc); rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); @@ -117,6 +122,7 @@ impl UiRenderState { children: Vec::new(), size_deps: Vec::new(), reads_output: false, + move_idx, rsc, }; @@ -134,6 +140,7 @@ impl UiRenderState { children, size_deps, reads_output, + move_idx, layer, id, } = painter; @@ -155,6 +162,7 @@ impl UiRenderState { children, size_deps, reads_output, + move_idx, mask, layer, }; @@ -171,6 +179,25 @@ impl UiRenderState { size } + /// The slot a widget's drawing is positioned through, made on its first + /// draw and kept until it stops being drawn. + fn move_slot( + &mut self, + id: WidgetId, + parent: Option, + rsc: &mut dyn UiRsc, + ) -> MoveIdx { + if let Some(&idx) = self.moves.get(&id) { + return idx; + } + let parent = parent + .and_then(|p| self.moves.get(&p).copied()) + .unwrap_or(MoveIdx::NONE); + let idx = rsc.ui_mut().moves.push(parent); + self.moves.insert(id, idx); + idx + } + /// The drawing a widget already has, kept for a new box if the box has not /// changed in a way it depends on. fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option { @@ -255,6 +282,9 @@ impl UiRenderState { active.textures.clear(); rsc.ui_mut().textures.free(); if undraw { + if let Some(idx) = self.moves.remove(&id) { + rsc.ui_mut().moves.remove(idx); + } rsc.on_undraw(active); } } @@ -275,6 +305,8 @@ impl UiRenderState { for (_, active) in self.active.drain() { rsc.on_undraw(&active); } + self.moves.clear(); + rsc.ui_mut().moves.clear(); self.layers.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); diff --git a/core/src/util/arena.rs b/core/src/util/arena.rs index 9ddfd99..46e5eca 100644 --- a/core/src/util/arena.rs +++ b/core/src/util/arena.rs @@ -34,6 +34,10 @@ impl Arena { self.tracker.free(id); self.data[i] } + + pub fn get_mut(&mut self, id: Id) -> &mut T { + &mut self.data[id.idx()] + } } impl Default for Arena { diff --git a/core/src/util/vec2.rs b/core/src/util/vec2.rs index a678216..8c67d02 100644 --- a/core/src/util/vec2.rs +++ b/core/src/util/vec2.rs @@ -1,7 +1,11 @@ use crate::util::impl_op; use std::{hash::Hash, ops::*}; -#[repr(C)] +/// `align(8)` because that is WGSL's alignment for a `vec2`, so any GPU +/// struct holding one is laid out the way its shader reads it without having +/// to say so itself. Those structs still need a manual `unsafe impl Pod`, +/// since the trailing padding this introduces is what `derive(Pod)` refuses. +#[repr(C, align(8))] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vec2 { pub x: f32, diff --git a/tests/draw_cost.rs b/tests/draw_cost.rs index 36a1005..589e8cf 100644 --- a/tests/draw_cost.rs +++ b/tests/draw_cost.rs @@ -22,8 +22,8 @@ use std::time::Instant; use iris::prelude::*; use iris_core::{ - GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData, - UiRegion, UiRenderNode, UiRenderState, + GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle, + TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState, }; use wgpu::{Color as GpuColor, *}; @@ -95,6 +95,7 @@ fn fill( primitive: RectPrimitive::color(UiColor::WHITE), region: UiRegion::FULL, mask_idx: MaskIdx::NONE, + move_idx: MoveIdx::NONE, }, ); render.layers.write( @@ -111,6 +112,7 @@ fn fill( }, region: UiRegion::FULL, mask_idx: MaskIdx::NONE, + move_idx: MoveIdx::NONE, }, ); } @@ -123,6 +125,7 @@ fn fill( primitive: TexturePrimitive::from(h), region: UiRegion::FULL, mask_idx: MaskIdx::NONE, + move_idx: MoveIdx::NONE, }, ); }