From 6fbdf9fbc803170079860de45cf2e00161f57969 Mon Sep 17 00:00:00 2001 From: Shadow Cat Date: Sat, 23 Aug 2025 13:02:00 -0400 Subject: [PATCH] texture freeing + render updates done a bit nicer --- src/layout/id.rs | 33 ++++++++----------- src/layout/texture.rs | 69 +++++++++++++++++++++++++++++---------- src/layout/ui.rs | 63 +++++++++++++++-------------------- src/render/mod.rs | 18 ++++------ src/render/primitive.rs | 12 ++++++- src/render/texture.rs | 40 ++++++++++++++++------- src/testing/mod.rs | 15 ++++----- src/testing/render/mod.rs | 4 +-- src/util/mod.rs | 3 ++ src/util/refcount.rs | 27 +++++++++++++++ 10 files changed, 176 insertions(+), 108 deletions(-) create mode 100644 src/util/refcount.rs diff --git a/src/layout/id.rs b/src/layout/id.rs index 8f6dbcd..6d6ca9f 100644 --- a/src/layout/id.rs +++ b/src/layout/id.rs @@ -1,14 +1,9 @@ -use std::{ - any::TypeId, - marker::PhantomData, - sync::{ - Arc, - atomic::{AtomicU32, Ordering}, - mpsc::Sender, - }, -}; +use std::{any::TypeId, marker::PhantomData}; -use crate::{FnTag, Ui, Widget, WidgetLike, WidgetTag, util::Id}; +use crate::{ + FnTag, Ui, UiMsg, UiMsgSender, Widget, WidgetLike, WidgetTag, + util::{Id, RefCounter}, +}; pub struct AnyWidget; @@ -22,8 +17,8 @@ pub struct AnyWidget; pub struct WidgetId { pub(super) ty: TypeId, pub(super) id: Id, - pub(super) refcount: Arc, - pub(super) send: Sender, + counter: RefCounter, + send: UiMsgSender, _pd: PhantomData, } @@ -35,11 +30,10 @@ impl std::fmt::Debug for WidgetId { impl Clone for WidgetId { fn clone(&self) -> Self { - self.refcount.fetch_add(1, Ordering::Release); Self { id: self.id.duplicate(), ty: self.ty, - refcount: self.refcount.clone(), + counter: self.counter.clone(), send: self.send.clone(), _pd: PhantomData, } @@ -47,11 +41,11 @@ impl Clone for WidgetId { } impl WidgetId { - pub(super) fn new(id: Id, ty: TypeId, send: Sender) -> Self { + pub(super) fn new(id: Id, ty: TypeId, send: UiMsgSender) -> Self { Self { ty, id, - refcount: AtomicU32::new(0).into(), + counter: RefCounter::new(), send, _pd: PhantomData, } @@ -72,15 +66,14 @@ impl WidgetId { } pub fn refs(&self) -> u32 { - self.refcount.load(Ordering::Acquire) + self.counter.refs() } } impl Drop for WidgetId { fn drop(&mut self) { - let refs = self.refcount.fetch_sub(1, Ordering::Release); - if refs == 0 { - let _ = self.send.send(self.id.duplicate()); + if self.counter.drop() { + let _ = self.send.send(UiMsg::FreeWidget(self.id.duplicate())); } } } diff --git a/src/layout/texture.rs b/src/layout/texture.rs index 8431d54..91f71c7 100644 --- a/src/layout/texture.rs +++ b/src/layout/texture.rs @@ -1,48 +1,81 @@ use image::DynamicImage; -use crate::render::TexturePrimitive; +use crate::{UiMsg, UiMsgSender, render::TexturePrimitive, util::RefCounter}; -/// TODO: proper resource management pub struct TextureHandle { pub inner: TexturePrimitive, + counter: RefCounter, + send: UiMsgSender, } /// a texture manager for a ui /// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped #[derive(Default)] pub struct Textures { - /// TODO: these are images, not views rn - views: Vec, - changed: bool, + free: Vec, + images: Vec>, + updates: Vec, } -pub struct TextureUpdates<'a> { - pub images: Option<&'a [DynamicImage]>, +pub enum TextureUpdate<'a> { + Push(&'a DynamicImage), + Set(u32, &'a DynamicImage), + Free(u32), +} + +enum Update { + Push(u32), + Set(u32), + Free(u32), } impl Textures { - pub fn add(&mut self, image: DynamicImage) -> TextureHandle { - let view_idx = self.views.len() as u32; - self.views.push(image); + pub fn add(&mut self, image: DynamicImage, send: UiMsgSender) -> TextureHandle { + let view_idx = self.push(image); // 0 == default in renderer; TODO: actually create samplers here let sampler_idx = 0; - self.changed = true; TextureHandle { inner: TexturePrimitive { view_idx, sampler_idx, }, + counter: RefCounter::new(), + send, } } - pub fn updates(&mut self) -> TextureUpdates<'_> { - if self.changed { - self.changed = false; - TextureUpdates { - images: Some(&self.views), - } + fn push(&mut self, image: DynamicImage) -> u32 { + if let Some(i) = self.free.pop() { + self.images[i as usize] = Some(image); + self.updates.push(Update::Set(i)); + i } else { - TextureUpdates { images: None } + let i = self.images.len() as u32; + self.images.push(Some(image)); + self.updates.push(Update::Push(i)); + i + } + } + + pub fn free(&mut self, idx: u32) { + self.images[idx as usize] = None; + self.updates.push(Update::Free(idx)); + self.free.push(idx); + } + + pub fn updates(&mut self) -> impl Iterator> { + self.updates.drain(..).map(|u| match u { + Update::Push(i) => TextureUpdate::Push(self.images[i as usize].as_ref().unwrap()), + Update::Set(i) => TextureUpdate::Set(i, self.images[i as usize].as_ref().unwrap()), + Update::Free(i) => TextureUpdate::Free(i), + }) + } +} + +impl Drop for TextureHandle { + fn drop(&mut self) { + if self.counter.drop() { + let _ = self.send.send(UiMsg::FreeTexture(self.inner.view_idx)); } } } diff --git a/src/layout/ui.rs b/src/layout/ui.rs index 3520104..4a9faca 100644 --- a/src/layout/ui.rs +++ b/src/layout/ui.rs @@ -1,8 +1,8 @@ use image::DynamicImage; use crate::{ - ActiveSensors, HashMap, Painter, SensorMap, TextureHandle, TextureUpdates, Textures, Widget, - WidgetId, WidgetLike, + ActiveSensors, HashMap, Painter, SensorMap, TextureHandle, Textures, Widget, WidgetId, + WidgetLike, render::Primitives, util::{Id, IdTracker}, }; @@ -16,16 +16,22 @@ pub struct Ui { base: Option, widgets: Widgets, updates: Vec, - del_recv: Receiver, - del_send: Sender, - primitives: Primitives, - textures: Textures, + recv: Receiver, + send: UiMsgSender, + pub(crate) primitives: Primitives, + pub(crate) textures: Textures, full_redraw: bool, pub(super) active_sensors: ActiveSensors, pub(super) sensor_map: SensorMap, } +pub enum UiMsg { + FreeWidget(Id), + FreeTexture(u32), +} +pub type UiMsgSender = Sender; + #[derive(Default)] pub struct Widgets { ids: IdTracker, @@ -75,15 +81,11 @@ impl Ui { } pub fn id>(&mut self) -> WidgetId { - WidgetId::new( - self.widgets.reserve(), - TypeId::of::(), - self.del_send.clone(), - ) + WidgetId::new(self.widgets.reserve(), TypeId::of::(), self.send.clone()) } pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle { - self.textures.add(image) + self.textures.add(image, self.send.clone()) } pub fn redraw_all(&mut self, ctx: &mut Ctx) @@ -103,33 +105,27 @@ impl Ui { self.primitives = painter.finish(); } - pub fn update(&mut self, ctx: &mut Ctx) -> UiRenderUpdates + pub fn update(&mut self, ctx: &mut Ctx) where Ctx: 'static, { - while let Ok(id) = self.del_recv.try_recv() { - self.widgets.delete(id); - } + self.recv_msgs(); if self.full_redraw { self.redraw_all(ctx); self.full_redraw = false; - UiRenderUpdates { - primitives: Some(&self.primitives), - textures: self.textures.updates(), - } - } else if self.updates.is_empty() { - UiRenderUpdates { - primitives: None, - textures: self.textures.updates(), - } - } else { + } else if !self.updates.is_empty() { // TODO: partial updates self.redraw_all(ctx); self.updates.drain(..); - UiRenderUpdates { - primitives: Some(&self.primitives), - textures: self.textures.updates(), + } + } + + fn recv_msgs(&mut self) { + while let Ok(msg) = self.recv.try_recv() { + match msg { + UiMsg::FreeWidget(id) => self.widgets.delete(id), + UiMsg::FreeTexture(id) => self.textures.free(id), } } } @@ -230,13 +226,8 @@ impl Default for Ui { full_redraw: false, active_sensors: Default::default(), sensor_map: Default::default(), - del_send, - del_recv, + send: del_send, + recv: del_recv, } } } - -pub struct UiRenderUpdates<'a> { - pub primitives: Option<&'a Primitives>, - pub textures: TextureUpdates<'a>, -} diff --git a/src/render/mod.rs b/src/render/mod.rs index 2c4e3b9..b5e66f8 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,7 +1,7 @@ use std::num::NonZero; use crate::{ - Ui, UiRenderUpdates, + Ui, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, }; use data::WindowUniform; @@ -21,7 +21,6 @@ pub use primitive::*; const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); pub struct UiRenderer { - uniform_layout: BindGroupLayout, uniform_group: BindGroup, primitive_layout: BindGroupLayout, primitive_group: BindGroup, @@ -33,8 +32,6 @@ pub struct UiRenderer { window_buffer: Buffer, instance: ArrBuf, primitives: PrimitiveBuffers, - - limits: UiLimits, textures: GpuTextures, } @@ -51,14 +48,15 @@ impl UiRenderer { pass.draw(0..4, 0..self.instance.len() as u32); } - pub fn update(&mut self, device: &Device, queue: &Queue, updates: UiRenderUpdates) { - if let Some(primitives) = updates.primitives { - self.instance.update(device, queue, &primitives.instances); - self.primitives.update(device, queue, &primitives.data); + pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) { + if ui.primitives.updated { + self.instance + .update(device, queue, &ui.primitives.instances); + self.primitives.update(device, queue, &ui.primitives.data); self.primitive_group = Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers()) } - if self.textures.apply(updates.textures) { + if self.textures.update(&mut ui.textures) { self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures) } } @@ -179,7 +177,6 @@ impl UiRenderer { }); Self { - uniform_layout, uniform_group, primitive_layout, primitive_group, @@ -189,7 +186,6 @@ impl UiRenderer { window_buffer, instance, primitives, - limits, textures: tex_manager, } } diff --git a/src/render/primitive.rs b/src/render/primitive.rs index 6b83721..9779de7 100644 --- a/src/render/primitive.rs +++ b/src/render/primitive.rs @@ -5,10 +5,20 @@ use crate::{ use bytemuck::Pod; use wgpu::*; -#[derive(Default)] pub struct Primitives { pub(super) instances: Vec, pub(super) data: PrimitiveData, + pub updated: bool, +} + +impl Default for Primitives { + fn default() -> Self { + Self { + instances: Default::default(), + data: Default::default(), + updated: true, + } + } } pub trait Primitive: Pod { diff --git a/src/render/texture.rs b/src/render/texture.rs index a5e83cf..658ffc8 100644 --- a/src/render/texture.rs +++ b/src/render/texture.rs @@ -1,28 +1,43 @@ use image::{DynamicImage, EncodableLayout}; use wgpu::{util::DeviceExt, *}; -use crate::TextureUpdates; +use crate::{TextureUpdate, Textures}; pub struct GpuTextures { device: Device, queue: Queue, views: Vec, samplers: Vec, + null_view: TextureView, no_views: Vec, } impl GpuTextures { - pub fn apply(&mut self, updates: TextureUpdates) -> bool { - if let Some(images) = updates.images { - for img in images { - self.add_view(img); + pub fn update(&mut self, textures: &mut Textures) -> bool { + let mut changed = false; + for update in textures.updates() { + changed = true; + match update { + TextureUpdate::Push(image) => self.push(image), + TextureUpdate::Set(i, image) => self.set(i, image), + TextureUpdate::Free(i) => self.free(i), } - true - } else { - false } + changed } - fn add_view(&mut self, image: &DynamicImage) { + fn set(&mut self, i: u32, image: &DynamicImage) { + let view = self.create_view(image); + self.views[i as usize] = view; + } + fn free(&mut self, i: u32) { + self.views[i as usize] = self.null_view.clone(); + } + fn push(&mut self, image: &DynamicImage) { + let view = self.create_view(image); + self.views.push(view); + } + + fn create_view(&self, image: &DynamicImage) -> TextureView { let image = image.to_rgba8(); let (width, height) = image.dimensions(); let texture = self.device.create_texture_with_data( @@ -44,17 +59,18 @@ impl GpuTextures { wgt::TextureDataOrder::MipMajor, image.as_bytes(), ); - let view = texture.create_view(&TextureViewDescriptor::default()); - self.views.push(view); + texture.create_view(&TextureViewDescriptor::default()) } pub fn new(device: &Device, queue: &Queue) -> Self { + let null_view = null_texture_view(device); Self { device: device.clone(), queue: queue.clone(), views: Vec::new(), samplers: vec![default_sampler(device)], - no_views: vec![null_texture_view(device)], + no_views: vec![null_view.clone()], + null_view, } } diff --git a/src/testing/mod.rs b/src/testing/mod.rs index fe8be2f..b06794d 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -76,8 +76,6 @@ impl Client { let span_add_test = ui.add(Span::empty(Dir::RIGHT).id(&span_add)); let main: WidgetId = ui.id(); - let image_test = ui.add(image(include_bytes!("assets/sungals.png"))); - fn switch_button( color: UiColor, main: &WidgetId, @@ -106,9 +104,8 @@ impl Client { switch_button(UiColor::RED, &main, &pad_test), switch_button(UiColor::GREEN, &main, &span_test), switch_button(UiColor::BLUE, &main, &span_add_test), - switch_button(UiColor::MAGENTA, &main, &image_test), ) - .span(Dir::RIGHT, [1, 1, 1, 1]), + .span(Dir::RIGHT, [1, 1, 1]), ); let test_button = Rect::new(Color::PURPLE) .radius(30) @@ -159,15 +156,17 @@ impl Client { match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::RedrawRequested => { - let updates = ui.update(self); - self.renderer.update(updates); + ui.update(self); + self.renderer.update(ui); self.renderer.draw() } WindowEvent::Resized(size) => self.renderer.resize(&size), WindowEvent::KeyboardInput { event, .. } => { if event.state.is_pressed() { - let child = ui.add(Rect::new(Color::YELLOW)).erase_type(); - ui[&self.ui.span_add].children.push((child, fixed(20.0))); + let child = ui + .add(image(include_bytes!("assets/sungals.png"))) + .erase_type(); + ui[&self.ui.span_add].children.push((child, ratio(1))); self.renderer.window().request_redraw(); } } diff --git a/src/testing/render/mod.rs b/src/testing/render/mod.rs index 44be282..9da158c 100644 --- a/src/testing/render/mod.rs +++ b/src/testing/render/mod.rs @@ -1,7 +1,7 @@ use pollster::FutureExt; use std::sync::Arc; use ui::{ - UiRenderUpdates, + Ui, render::{UiLimits, UiRenderer}, }; use wgpu::{util::StagingBelt, *}; @@ -21,7 +21,7 @@ pub struct Renderer { } impl Renderer { - pub fn update(&mut self, updates: UiRenderUpdates) { + pub fn update(&mut self, updates: &mut Ui) { self.ui.update(&self.device, &self.queue, updates); } diff --git a/src/util/mod.rs b/src/util/mod.rs index bd5aaa0..13decb4 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,8 @@ mod id; mod math; +mod refcount; pub use id::*; pub use math::*; +pub use refcount::*; + diff --git a/src/util/refcount.rs b/src/util/refcount.rs new file mode 100644 index 0000000..064a7a1 --- /dev/null +++ b/src/util/refcount.rs @@ -0,0 +1,27 @@ +use std::sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + mpsc::Sender, +}; + +pub struct RefCounter(Arc); + +impl RefCounter { + pub fn new() -> Self { + Self(Arc::new(0.into())) + } + pub fn refs(&self) -> u32 { + self.0.load(Ordering::Acquire) + } + pub fn drop(&mut self) -> bool { + let refs = self.0.fetch_sub(1, Ordering::Release); + refs == 0 + } +} + +impl Clone for RefCounter { + fn clone(&self) -> Self { + self.0.fetch_add(1, Ordering::Release); + Self(self.0.clone()) + } +}