texture freeing + render updates done a bit nicer

This commit is contained in:
2025-08-23 13:02:00 -04:00
parent 5fe63e311c
commit 6fbdf9fbc8
10 changed files with 176 additions and 108 deletions

View File

@@ -1,14 +1,9 @@
use std::{ use std::{any::TypeId, marker::PhantomData};
any::TypeId,
marker::PhantomData,
sync::{
Arc,
atomic::{AtomicU32, Ordering},
mpsc::Sender,
},
};
use crate::{FnTag, Ui, Widget, WidgetLike, WidgetTag, util::Id}; use crate::{
FnTag, Ui, UiMsg, UiMsgSender, Widget, WidgetLike, WidgetTag,
util::{Id, RefCounter},
};
pub struct AnyWidget; pub struct AnyWidget;
@@ -22,8 +17,8 @@ pub struct AnyWidget;
pub struct WidgetId<W = AnyWidget> { pub struct WidgetId<W = AnyWidget> {
pub(super) ty: TypeId, pub(super) ty: TypeId,
pub(super) id: Id, pub(super) id: Id,
pub(super) refcount: Arc<AtomicU32>, counter: RefCounter,
pub(super) send: Sender<Id>, send: UiMsgSender,
_pd: PhantomData<W>, _pd: PhantomData<W>,
} }
@@ -35,11 +30,10 @@ impl<W> std::fmt::Debug for WidgetId<W> {
impl<W> Clone for WidgetId<W> { impl<W> Clone for WidgetId<W> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
self.refcount.fetch_add(1, Ordering::Release);
Self { Self {
id: self.id.duplicate(), id: self.id.duplicate(),
ty: self.ty, ty: self.ty,
refcount: self.refcount.clone(), counter: self.counter.clone(),
send: self.send.clone(), send: self.send.clone(),
_pd: PhantomData, _pd: PhantomData,
} }
@@ -47,11 +41,11 @@ impl<W> Clone for WidgetId<W> {
} }
impl<W> WidgetId<W> { impl<W> WidgetId<W> {
pub(super) fn new(id: Id, ty: TypeId, send: Sender<Id>) -> Self { pub(super) fn new(id: Id, ty: TypeId, send: UiMsgSender) -> Self {
Self { Self {
ty, ty,
id, id,
refcount: AtomicU32::new(0).into(), counter: RefCounter::new(),
send, send,
_pd: PhantomData, _pd: PhantomData,
} }
@@ -72,15 +66,14 @@ impl<W> WidgetId<W> {
} }
pub fn refs(&self) -> u32 { pub fn refs(&self) -> u32 {
self.refcount.load(Ordering::Acquire) self.counter.refs()
} }
} }
impl<W> Drop for WidgetId<W> { impl<W> Drop for WidgetId<W> {
fn drop(&mut self) { fn drop(&mut self) {
let refs = self.refcount.fetch_sub(1, Ordering::Release); if self.counter.drop() {
if refs == 0 { let _ = self.send.send(UiMsg::FreeWidget(self.id.duplicate()));
let _ = self.send.send(self.id.duplicate());
} }
} }
} }

View File

@@ -1,48 +1,81 @@
use image::DynamicImage; use image::DynamicImage;
use crate::render::TexturePrimitive; use crate::{UiMsg, UiMsgSender, render::TexturePrimitive, util::RefCounter};
/// TODO: proper resource management
pub struct TextureHandle { pub struct TextureHandle {
pub inner: TexturePrimitive, pub inner: TexturePrimitive,
counter: RefCounter,
send: UiMsgSender,
} }
/// a texture manager for a ui /// a texture manager for a ui
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped /// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
#[derive(Default)] #[derive(Default)]
pub struct Textures { pub struct Textures {
/// TODO: these are images, not views rn free: Vec<u32>,
views: Vec<DynamicImage>, images: Vec<Option<DynamicImage>>,
changed: bool, updates: Vec<Update>,
} }
pub struct TextureUpdates<'a> { pub enum TextureUpdate<'a> {
pub images: Option<&'a [DynamicImage]>, Push(&'a DynamicImage),
Set(u32, &'a DynamicImage),
Free(u32),
}
enum Update {
Push(u32),
Set(u32),
Free(u32),
} }
impl Textures { impl Textures {
pub fn add(&mut self, image: DynamicImage) -> TextureHandle { pub fn add(&mut self, image: DynamicImage, send: UiMsgSender) -> TextureHandle {
let view_idx = self.views.len() as u32; let view_idx = self.push(image);
self.views.push(image);
// 0 == default in renderer; TODO: actually create samplers here // 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0; let sampler_idx = 0;
self.changed = true;
TextureHandle { TextureHandle {
inner: TexturePrimitive { inner: TexturePrimitive {
view_idx, view_idx,
sampler_idx, sampler_idx,
}, },
counter: RefCounter::new(),
send,
} }
} }
pub fn updates(&mut self) -> TextureUpdates<'_> { fn push(&mut self, image: DynamicImage) -> u32 {
if self.changed { if let Some(i) = self.free.pop() {
self.changed = false; self.images[i as usize] = Some(image);
TextureUpdates { self.updates.push(Update::Set(i));
images: Some(&self.views), i
}
} else { } 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<Item = TextureUpdate<'_>> {
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));
} }
} }
} }

View File

@@ -1,8 +1,8 @@
use image::DynamicImage; use image::DynamicImage;
use crate::{ use crate::{
ActiveSensors, HashMap, Painter, SensorMap, TextureHandle, TextureUpdates, Textures, Widget, ActiveSensors, HashMap, Painter, SensorMap, TextureHandle, Textures, Widget, WidgetId,
WidgetId, WidgetLike, WidgetLike,
render::Primitives, render::Primitives,
util::{Id, IdTracker}, util::{Id, IdTracker},
}; };
@@ -16,16 +16,22 @@ pub struct Ui<Ctx> {
base: Option<WidgetId>, base: Option<WidgetId>,
widgets: Widgets<Ctx>, widgets: Widgets<Ctx>,
updates: Vec<WidgetId>, updates: Vec<WidgetId>,
del_recv: Receiver<Id>, recv: Receiver<UiMsg>,
del_send: Sender<Id>, send: UiMsgSender,
primitives: Primitives, pub(crate) primitives: Primitives,
textures: Textures, pub(crate) textures: Textures,
full_redraw: bool, full_redraw: bool,
pub(super) active_sensors: ActiveSensors, pub(super) active_sensors: ActiveSensors,
pub(super) sensor_map: SensorMap<Ctx>, pub(super) sensor_map: SensorMap<Ctx>,
} }
pub enum UiMsg {
FreeWidget(Id),
FreeTexture(u32),
}
pub type UiMsgSender = Sender<UiMsg>;
#[derive(Default)] #[derive(Default)]
pub struct Widgets<Ctx> { pub struct Widgets<Ctx> {
ids: IdTracker, ids: IdTracker,
@@ -75,15 +81,11 @@ impl<Ctx> Ui<Ctx> {
} }
pub fn id<W: Widget<Ctx>>(&mut self) -> WidgetId<W> { pub fn id<W: Widget<Ctx>>(&mut self) -> WidgetId<W> {
WidgetId::new( WidgetId::new(self.widgets.reserve(), TypeId::of::<W>(), self.send.clone())
self.widgets.reserve(),
TypeId::of::<W>(),
self.del_send.clone(),
)
} }
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle { 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) pub fn redraw_all(&mut self, ctx: &mut Ctx)
@@ -103,33 +105,27 @@ impl<Ctx> Ui<Ctx> {
self.primitives = painter.finish(); self.primitives = painter.finish();
} }
pub fn update(&mut self, ctx: &mut Ctx) -> UiRenderUpdates pub fn update(&mut self, ctx: &mut Ctx)
where where
Ctx: 'static, Ctx: 'static,
{ {
while let Ok(id) = self.del_recv.try_recv() { self.recv_msgs();
self.widgets.delete(id);
}
if self.full_redraw { if self.full_redraw {
self.redraw_all(ctx); self.redraw_all(ctx);
self.full_redraw = false; self.full_redraw = false;
UiRenderUpdates { } else if !self.updates.is_empty() {
primitives: Some(&self.primitives),
textures: self.textures.updates(),
}
} else if self.updates.is_empty() {
UiRenderUpdates {
primitives: None,
textures: self.textures.updates(),
}
} else {
// TODO: partial updates // TODO: partial updates
self.redraw_all(ctx); self.redraw_all(ctx);
self.updates.drain(..); 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<Ctx: 'static> Default for Ui<Ctx> {
full_redraw: false, full_redraw: false,
active_sensors: Default::default(), active_sensors: Default::default(),
sensor_map: Default::default(), sensor_map: Default::default(),
del_send, send: del_send,
del_recv, recv: del_recv,
} }
} }
} }
pub struct UiRenderUpdates<'a> {
pub primitives: Option<&'a Primitives>,
pub textures: TextureUpdates<'a>,
}

View File

@@ -1,7 +1,7 @@
use std::num::NonZero; use std::num::NonZero;
use crate::{ use crate::{
Ui, UiRenderUpdates, Ui,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
}; };
use data::WindowUniform; use data::WindowUniform;
@@ -21,7 +21,6 @@ pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UiRenderer { pub struct UiRenderer {
uniform_layout: BindGroupLayout,
uniform_group: BindGroup, uniform_group: BindGroup,
primitive_layout: BindGroupLayout, primitive_layout: BindGroupLayout,
primitive_group: BindGroup, primitive_group: BindGroup,
@@ -33,8 +32,6 @@ pub struct UiRenderer {
window_buffer: Buffer, window_buffer: Buffer,
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers, primitives: PrimitiveBuffers,
limits: UiLimits,
textures: GpuTextures, textures: GpuTextures,
} }
@@ -51,14 +48,15 @@ impl UiRenderer {
pass.draw(0..4, 0..self.instance.len() as u32); pass.draw(0..4, 0..self.instance.len() as u32);
} }
pub fn update(&mut self, device: &Device, queue: &Queue, updates: UiRenderUpdates) { pub fn update<Ctx>(&mut self, device: &Device, queue: &Queue, ui: &mut Ui<Ctx>) {
if let Some(primitives) = updates.primitives { if ui.primitives.updated {
self.instance.update(device, queue, &primitives.instances); self.instance
self.primitives.update(device, queue, &primitives.data); .update(device, queue, &ui.primitives.instances);
self.primitives.update(device, queue, &ui.primitives.data);
self.primitive_group = self.primitive_group =
Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers()) 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) self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures)
} }
} }
@@ -179,7 +177,6 @@ impl UiRenderer {
}); });
Self { Self {
uniform_layout,
uniform_group, uniform_group,
primitive_layout, primitive_layout,
primitive_group, primitive_group,
@@ -189,7 +186,6 @@ impl UiRenderer {
window_buffer, window_buffer,
instance, instance,
primitives, primitives,
limits,
textures: tex_manager, textures: tex_manager,
} }
} }

View File

@@ -5,10 +5,20 @@ use crate::{
use bytemuck::Pod; use bytemuck::Pod;
use wgpu::*; use wgpu::*;
#[derive(Default)]
pub struct Primitives { pub struct Primitives {
pub(super) instances: Vec<PrimitiveInstance>, pub(super) instances: Vec<PrimitiveInstance>,
pub(super) data: PrimitiveData, 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 { pub trait Primitive: Pod {

View File

@@ -1,28 +1,43 @@
use image::{DynamicImage, EncodableLayout}; use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::TextureUpdates; use crate::{TextureUpdate, Textures};
pub struct GpuTextures { pub struct GpuTextures {
device: Device, device: Device,
queue: Queue, queue: Queue,
views: Vec<TextureView>, views: Vec<TextureView>,
samplers: Vec<Sampler>, samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>, no_views: Vec<TextureView>,
} }
impl GpuTextures { impl GpuTextures {
pub fn apply(&mut self, updates: TextureUpdates) -> bool { pub fn update(&mut self, textures: &mut Textures) -> bool {
if let Some(images) = updates.images { let mut changed = false;
for img in images { for update in textures.updates() {
self.add_view(img); 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 image = image.to_rgba8();
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
@@ -44,17 +59,18 @@ impl GpuTextures {
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
image.as_bytes(), image.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); texture.create_view(&TextureViewDescriptor::default())
self.views.push(view);
} }
pub fn new(device: &Device, queue: &Queue) -> Self { pub fn new(device: &Device, queue: &Queue) -> Self {
let null_view = null_texture_view(device);
Self { Self {
device: device.clone(), device: device.clone(),
queue: queue.clone(), queue: queue.clone(),
views: Vec::new(), views: Vec::new(),
samplers: vec![default_sampler(device)], samplers: vec![default_sampler(device)],
no_views: vec![null_texture_view(device)], no_views: vec![null_view.clone()],
null_view,
} }
} }

View File

@@ -76,8 +76,6 @@ impl Client {
let span_add_test = ui.add(Span::empty(Dir::RIGHT).id(&span_add)); let span_add_test = ui.add(Span::empty(Dir::RIGHT).id(&span_add));
let main: WidgetId<Regioned> = ui.id(); let main: WidgetId<Regioned> = ui.id();
let image_test = ui.add(image(include_bytes!("assets/sungals.png")));
fn switch_button<To>( fn switch_button<To>(
color: UiColor, color: UiColor,
main: &WidgetId<Regioned>, main: &WidgetId<Regioned>,
@@ -106,9 +104,8 @@ impl Client {
switch_button(UiColor::RED, &main, &pad_test), switch_button(UiColor::RED, &main, &pad_test),
switch_button(UiColor::GREEN, &main, &span_test), switch_button(UiColor::GREEN, &main, &span_test),
switch_button(UiColor::BLUE, &main, &span_add_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) let test_button = Rect::new(Color::PURPLE)
.radius(30) .radius(30)
@@ -159,15 +156,17 @@ impl Client {
match event { match event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
let updates = ui.update(self); ui.update(self);
self.renderer.update(updates); self.renderer.update(ui);
self.renderer.draw() self.renderer.draw()
} }
WindowEvent::Resized(size) => self.renderer.resize(&size), WindowEvent::Resized(size) => self.renderer.resize(&size),
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
if event.state.is_pressed() { if event.state.is_pressed() {
let child = ui.add(Rect::new(Color::YELLOW)).erase_type(); let child = ui
ui[&self.ui.span_add].children.push((child, fixed(20.0))); .add(image(include_bytes!("assets/sungals.png")))
.erase_type();
ui[&self.ui.span_add].children.push((child, ratio(1)));
self.renderer.window().request_redraw(); self.renderer.window().request_redraw();
} }
} }

View File

@@ -1,7 +1,7 @@
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use ui::{ use ui::{
UiRenderUpdates, Ui,
render::{UiLimits, UiRenderer}, render::{UiLimits, UiRenderer},
}; };
use wgpu::{util::StagingBelt, *}; use wgpu::{util::StagingBelt, *};
@@ -21,7 +21,7 @@ pub struct Renderer {
} }
impl Renderer { impl Renderer {
pub fn update(&mut self, updates: UiRenderUpdates) { pub fn update<Ctx>(&mut self, updates: &mut Ui<Ctx>) {
self.ui.update(&self.device, &self.queue, updates); self.ui.update(&self.device, &self.queue, updates);
} }

View File

@@ -1,5 +1,8 @@
mod id; mod id;
mod math; mod math;
mod refcount;
pub use id::*; pub use id::*;
pub use math::*; pub use math::*;
pub use refcount::*;

27
src/util/refcount.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::sync::{
Arc,
atomic::{AtomicU32, Ordering},
mpsc::Sender,
};
pub struct RefCounter(Arc<AtomicU32>);
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())
}
}