From f54b218cbbfc0a8b2976fe7659a6a290f5927ce5 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 21 Feb 2024 20:13:58 +0100 Subject: [PATCH] WIP single draw call architecture --- hui-glium/src/lib.rs | 143 ++++++++++++++++++++---------------------- hui/src/draw.rs | 118 +++++++--------------------------- hui/src/draw/atlas.rs | 106 +++++++++++++++++++++++++++++++ hui/src/instance.rs | 22 +++---- 4 files changed, 208 insertions(+), 181 deletions(-) create mode 100644 hui/src/draw/atlas.rs diff --git a/hui-glium/src/lib.rs b/hui-glium/src/lib.rs index 4652b0c..3f526e4 100644 --- a/hui-glium/src/lib.rs +++ b/hui-glium/src/lib.rs @@ -11,7 +11,7 @@ use glium::{ }; use hui::{ UiInstance, - draw::{UiDrawPlan, UiVertex, BindTexture}, + draw::{UiDrawCall, UiVertex, BindTexture}, text::FontTextureInfo, IfModified, }; @@ -48,7 +48,7 @@ struct BufferPair { impl BufferPair { pub fn new(facade: &F) -> Self { - log::debug!("init ui buffers..."); + log::debug!("init ui buffers (empty)..."); Self { vertex_buffer: VertexBuffer::empty_dynamic(facade, 1024).unwrap(), index_buffer: IndexBuffer::empty_dynamic(facade, PrimitiveType::TrianglesList, 1024).unwrap(), @@ -57,6 +57,16 @@ impl BufferPair { } } + pub fn new_with_data(facade: &F, vtx: &[Vertex], idx: &[u32]) -> Self { + log::debug!("init ui buffers (data)..."); + Self { + vertex_buffer: VertexBuffer::dynamic(facade, vtx).unwrap(), + index_buffer: IndexBuffer::dynamic(facade, PrimitiveType::TrianglesList, idx).unwrap(), + vertex_count: 0, + index_count: 0, + } + } + pub fn ensure_buffer_size(&mut self, need_vtx: usize, need_idx: usize) { let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::(); let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::(); @@ -106,18 +116,12 @@ impl BufferPair { } } -struct GlDrawCall { - active: bool, - buffer: BufferPair, - bind_texture: Option>, -} - pub struct GliumUiRenderer { context: Rc, program: glium::Program, program_tex: glium::Program, - font_texture: Option>, - plan: Vec, + ui_texture: Option>, + buffer_pair: Option, } impl GliumUiRenderer { @@ -127,44 +131,33 @@ impl GliumUiRenderer { program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(), program_tex: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER_TEX, None).unwrap(), context: Rc::clone(facade.get_context()), - font_texture: None, - plan: vec![] + ui_texture: None, + buffer_pair: None, } } - pub fn update_draw_plan(&mut self, plan: &UiDrawPlan) { - if plan.calls.len() > self.plan.len() { - self.plan.resize_with(plan.calls.len(), || { - GlDrawCall { - buffer: BufferPair::new(&self.context), - bind_texture: None, - active: false, - } - }); - } else { - for step in &mut self.plan[plan.calls.len()..] { - step.active = false; - } - } - for (idx, call) in plan.calls.iter().enumerate() { - let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::>()[..]; - let data_idx = &call.indices[..]; - self.plan[idx].active = true; - self.plan[idx].buffer.write_data(data_vtx, data_idx); - self.plan[idx].bind_texture = match call.bind_texture { - Some(BindTexture::FontTexture) => { - const NO_FNT_TEX: &str = "Font texture exists in draw plan but not yet inited. Make sure to call update_font_texture() *before* update_draw_plan()"; - Some(Rc::clone(self.font_texture.as_ref().expect(NO_FNT_TEX))) - }, - Some(BindTexture::UserDefined(_)) => todo!("user defined textures are not implemented yet"), - None => None, - } + pub fn update_draw_plan(&mut self, call: &UiDrawCall) { + let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::>()[..]; + let data_idx = &call.indices[..]; + if let Some(buffer) = &mut self.buffer_pair { + buffer.write_data(data_vtx, data_idx); + } else if !call.indices.is_empty() { + self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx)); } + + // self.plan[0].bind_texture = match call.bind_texture { + // Some(BindTexture::FontTexture) => { + // const NO_FNT_TEX: &str = "Font texture exists in draw plan but not yet inited. Make sure to call update_font_texture() *before* update_draw_plan()"; + // Some(Rc::clone(self.font_texture.as_ref().expect(NO_FNT_TEX))) + // }, + // Some(BindTexture::UserDefined(_)) => todo!("user defined textures are not implemented yet"), + // None => None, + // } } - pub fn update_font_texture(&mut self, font_texture: &FontTextureInfo) { + pub fn update_ui_texture(&mut self, font_texture: &FontTextureInfo) { log::debug!("updating font texture"); - self.font_texture = Some(Rc::new(SrgbTexture2d::new( + self.ui_texture = Some(Rc::new(SrgbTexture2d::new( &self.context, RawImage2d::from_raw_rgba( font_texture.data.to_owned(), @@ -175,9 +168,9 @@ impl GliumUiRenderer { pub fn update(&mut self, hui: &UiInstance) { if let Some(texture) = hui.font_texture().if_modified() { - self.update_font_texture(texture); + self.update_ui_texture(texture); } - if let Some(plan) = hui.draw_plan().if_modified() { + if let Some(plan) = hui.draw_call().if_modified() { self.update_draw_plan(plan); } } @@ -188,43 +181,41 @@ impl GliumUiRenderer { ..Default::default() }; - for step in &self.plan { - if !step.active { - continue + if let Some(buffer) = &self.buffer_pair { + if buffer.is_empty() { + return } - if step.buffer.is_empty() { - continue - } + let vtx_buffer = buffer.vertex_buffer.slice(0..buffer.vertex_count).unwrap(); + let idx_buffer = buffer.index_buffer.slice(0..buffer.index_count).unwrap(); - let vtx_buffer = step.buffer.vertex_buffer.slice(0..step.buffer.vertex_count).unwrap(); - let idx_buffer = step.buffer.index_buffer.slice(0..step.buffer.index_count).unwrap(); + frame.draw( + vtx_buffer, + idx_buffer, + &self.program_tex, + &uniform! { + resolution: resolution.to_array(), + tex: Sampler(self.ui_texture.as_ref().unwrap().as_ref(), SamplerBehavior { + wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp), + ..Default::default() + }), + }, + ¶ms, + ).unwrap(); - if let Some(bind_texture) = step.bind_texture.as_ref() { - frame.draw( - vtx_buffer, - idx_buffer, - &self.program_tex, - &uniform! { - resolution: resolution.to_array(), - tex: Sampler(bind_texture.as_ref(), SamplerBehavior { - wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp), - ..Default::default() - }), - }, - ¶ms, - ).unwrap(); - } else { - frame.draw( - vtx_buffer, - idx_buffer, - &self.program, - &uniform! { - resolution: resolution.to_array(), - }, - ¶ms, - ).unwrap(); - } + // if let Some(bind_texture) = call.bind_texture.as_ref() { + + // } else { + // frame.draw( + // vtx_buffer, + // idx_buffer, + // &self.program, + // &uniform! { + // resolution: resolution.to_array(), + // }, + // ¶ms, + // ).unwrap(); + // } } } } diff --git a/hui/src/draw.rs b/hui/src/draw.rs index 99f2077..0198a15 100644 --- a/hui/src/draw.rs +++ b/hui/src/draw.rs @@ -6,6 +6,7 @@ use crate::{ IfModified }; +mod atlas; mod corner_radius; pub use corner_radius::RoundedCorners; @@ -54,16 +55,6 @@ pub enum UiDrawCommand { }, } -impl UiDrawCommand { - fn texture_eq_index(&self) -> u64 { - match self { - UiDrawCommand::Rectangle { .. } | - UiDrawCommand::Circle { .. } => u64::MAX - 1, - UiDrawCommand::Text { .. } => u64::MAX - 2, - } - } -} - /// List of draw commands #[derive(Default)] pub struct UiDrawCommandList { @@ -110,80 +101,22 @@ pub struct UiVertex { pub struct UiDrawCall { pub vertices: Vec, pub indices: Vec, - pub bind_texture: Option, } -/// Represents a complete UI rendering plan (a list of optimized draw calls). -#[derive(Default)] -pub struct UiDrawPlan { - pub calls: Vec -} - -struct CallSwapper { - calls: Vec, - call: UiDrawCall, -} - -impl CallSwapper { - pub fn new() -> Self { - Self { - calls: vec![], - call: UiDrawCall::default(), - } - } - - pub fn current(&self) -> &UiDrawCall { - &self.call - } - - pub fn current_mut(&mut self) -> &mut UiDrawCall { - &mut self.call - } - - pub fn swap(&mut self) { - self.calls.push(std::mem::take(&mut self.call)); - } - - pub fn finish(mut self) -> Vec { - self.calls.push(self.call); - self.calls - } -} - -impl UiDrawPlan { +impl UiDrawCall { /// Tesselate the UI and build a complete draw plan from a list of draw commands pub fn build(draw_commands: &UiDrawCommandList, tr: &mut TextRenderer) -> Self { - let mut swapper = CallSwapper::new(); - let mut prev_command: Option<&UiDrawCommand> = None; + let mut draw_call = UiDrawCall::default(); for command in &draw_commands.commands { - let do_swap = if let Some(prev_command) = prev_command { - //std::mem::discriminant(prev_command) != std::mem::discriminant(command) - prev_command.texture_eq_index() != command.texture_eq_index() - } else { - false - }; - - if do_swap { - swapper.swap(); - } - - if do_swap || prev_command.is_none() { - swapper.current_mut().bind_texture = match command { - UiDrawCommand::Rectangle { .. } | - UiDrawCommand::Circle { .. } => None, - UiDrawCommand::Text { .. } => Some(BindTexture::FontTexture), - } - } - match command { UiDrawCommand::Rectangle { position, size, color, rounded_corners } => { - let vidx = swapper.current().vertices.len() as u32; + let vidx = draw_call.vertices.len() as u32; if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) { //this code is stupid as fuck //Random vert in the center for no reason //lol - swapper.current_mut().vertices.push(UiVertex { + draw_call.vertices.push(UiVertex { position: *position + *size * vec2(0.5, 0.5), color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4., uv: vec2(0., 0.), @@ -197,32 +130,32 @@ impl UiDrawPlan { let x = angle.sin(); let y = angle.cos(); //Top-right corner - swapper.current_mut().vertices.push(UiVertex { + draw_call.vertices.push(UiVertex { position: *position + vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.), color: color.top_right, uv: vec2(0.0, 0.0), }); //Bottom-right corner - swapper.current_mut().vertices.push(UiVertex { + draw_call.vertices.push(UiVertex { position: *position + vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right), color: color.bottom_right, uv: vec2(0.0, 0.0), }); //Bottom-left corner - swapper.current_mut().vertices.push(UiVertex { + draw_call.vertices.push(UiVertex { position: *position + vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left), color: color.bottom_left, uv: vec2(0.0, 0.0), }); //Top-left corner - swapper.current_mut().vertices.push(UiVertex { + draw_call.vertices.push(UiVertex { position: *position + vec2(1. - x, 1. - y) * corner.radius.top_left, color: color.top_left, uv: vec2(0.0, 0.0), }); // mental illness: if i > 0 { - swapper.current_mut().indices.extend([ + draw_call.indices.extend([ //Top-right corner vidx, vidx + 1 + (i - 1) * 4, @@ -244,7 +177,7 @@ impl UiDrawPlan { } //Fill in the rest //mental illness 2: - swapper.current_mut().indices.extend([ + draw_call.indices.extend([ //Top vidx, vidx + 4, @@ -263,8 +196,8 @@ impl UiDrawPlan { vidx + 2, ]); } else { - swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); - swapper.current_mut().vertices.extend([ + draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); + draw_call.vertices.extend([ UiVertex { position: *position, color: color.top_left, @@ -313,15 +246,15 @@ impl UiDrawPlan { tr.font_texture().size.x as f32, tr.font_texture().size.y as f32 ); - let vidx = swapper.current().vertices.len() as u32; + let vidx = draw_call.vertices.len() as u32; let glyph = tr.glyph(*font, layout_glyph.parent, layout_glyph.key.px as u8); //rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width; - swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); + draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); let p0x = glyph.position.x as f32 / font_texture_size.0; let p1x = (glyph.position.x + glyph.size.x as i32) as f32 / font_texture_size.0; let p0y = glyph.position.y as f32 / font_texture_size.1; let p1y = (glyph.position.y + glyph.size.y as i32) as f32 / font_texture_size.1; - swapper.current_mut().vertices.extend([ + draw_call.vertices.extend([ UiVertex { position: *position + vec2(layout_glyph.x, layout_glyph.y), color: *color, @@ -347,27 +280,24 @@ impl UiDrawPlan { feature = "pixel_perfect_text", not(feature = "pixel_perfect") ))] { - for vtx in &mut swapper.current_mut().vertices[(vidx as usize)..] { + for vtx in &mut draw_call.vertices[(vidx as usize)..] { vtx.position = vtx.position.round() } } } } } - #[cfg(feature = "pixel_perfect")] - swapper.current_mut().vertices.iter_mut().for_each(|v| { - v.position = v.position.round() - }); - prev_command = Some(command); - } - Self { - calls: swapper.finish() } + #[cfg(feature = "pixel_perfect")] + draw_call.vertices.iter_mut().for_each(|v| { + v.position = v.position.round() + }); + draw_call } } -impl IfModified for (bool, &UiDrawPlan) { - fn if_modified(&self) -> Option<&UiDrawPlan> { +impl IfModified for (bool, &UiDrawCall) { + fn if_modified(&self) -> Option<&UiDrawCall> { match self.0 { true => Some(self.1), false => None, diff --git a/hui/src/draw/atlas.rs b/hui/src/draw/atlas.rs new file mode 100644 index 0000000..ca08402 --- /dev/null +++ b/hui/src/draw/atlas.rs @@ -0,0 +1,106 @@ +use glam::UVec2; +use hashbrown::HashMap; +use nohash_hasher::BuildNoHashHasher; +use rect_packer::DensePacker; + +const CHANNEL_COUNT: u32 = 4; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TextureHandle { + //TODO automatic cleanup when handle is dropped + //man: Weak>, + pub(crate) index: u32 +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct TextureAllocation { + /// Index of the texture allocation + pub index: u32, + + /// Position in the texture atlas + pub position: UVec2, + + /// Requested texture size + pub size: UVec2, + + /// True if the texture was rotated by 90 degrees + pub rotated: bool, +} + +pub(crate) struct TextureAtlasManager { + packer: DensePacker, + count: u32, + size: UVec2, + data: Vec, + allocations: HashMap>, +} + +impl TextureAtlasManager { + pub fn new(size: UVec2) -> Self { + Self { + packer: DensePacker::new(size.x as i32, size.y as i32), + count: 0, + size: UVec2::new(0, 0), + data: Vec::new(), + allocations: HashMap::default(), + } + } + + pub fn resize(&mut self, new_size: UVec2) { + if new_size.x > self.size.x && new_size.y > self.size.y{ + self.packer.resize(new_size.x as i32, new_size.y as i32); + //Resize the data array in-place + self.data.resize((new_size.x * new_size.y * CHANNEL_COUNT) as usize, 0); + for y in (1..self.size.y).rev() { + for x in (0..self.size.x).rev() { + let idx = (y * self.size.x + x) as usize; + let new_idx = (y * new_size.x + x) as usize; + self.data[new_idx] = self.data[idx]; + } + } + } else { + //If scaling down, just recreate the atlas from scratch (since we need to re-pack everything anyway) + todo!("Atlas downscaling is not implemented yet"); + } + self.size = new_size; + } + + /// Allocate a new texture region in the atlas + pub fn allocate(&mut self, size: UVec2) -> Option { + let result = self.packer.pack(size.x as i32, size.y as i32, true)?; + let index = self.count; + self.count += 1; + let allocation = TextureAllocation { + index, + position: UVec2::new(result.x as u32, result.y as u32), + size, + //If the size does not match the requested size, the texture was rotated + rotated: result.width != size.x as i32, + }; + self.allocations.insert_unique_unchecked(index, allocation); + Some(TextureHandle { index }) + } + + /// Allocate a new texture region in the atlas and copy the data into it + pub fn add(&mut self, width: u32, data: &[u8]) { + todo!() + } + + pub fn modify(&mut self, handle: TextureHandle) { + todo!() + } + + pub fn remove(&mut self, handle: TextureHandle) { + todo!() + } + + pub fn get(&self, handle: TextureHandle) -> Option<&TextureAllocation> { + self.allocations.get(&handle.index) + } +} + +impl Default for TextureAtlasManager { + fn default() -> Self { + Self::new(UVec2::new(512, 512)) + } +} diff --git a/hui/src/instance.rs b/hui/src/instance.rs index e502771..7c9739b 100644 --- a/hui/src/instance.rs +++ b/hui/src/instance.rs @@ -5,7 +5,7 @@ use crate:: { element::{MeasureContext, ProcessContext, UiElement}, event::UiEvent, state::StateRepo, - draw::{UiDrawCommandList, UiDrawPlan}, + draw::{UiDrawCommandList, UiDrawCall}, text::{TextRenderer, FontTextureInfo, FontHandle}, }; @@ -17,8 +17,8 @@ pub struct UiInstance { //event_queue: VecDeque, prev_draw_commands: UiDrawCommandList, draw_commands: UiDrawCommandList, - draw_plan: UiDrawPlan, - draw_plan_modified: bool, + draw_call: UiDrawCall, + draw_call_modified: bool, text_renderer: TextRenderer, events: VecDeque, } @@ -35,8 +35,8 @@ impl UiInstance { // root_elements: Vec::new(), prev_draw_commands: UiDrawCommandList::default(), draw_commands: UiDrawCommandList::default(), - draw_plan: UiDrawPlan::default(), - draw_plan_modified: false, + draw_call: UiDrawCall::default(), + draw_call_modified: false, // ftm: FontTextureManager::default(), text_renderer: TextRenderer::new(), events: VecDeque::new(), @@ -78,7 +78,7 @@ impl UiInstance { /// You must call this function at the beginning of the frame, before adding any elements pub fn begin(&mut self) { std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands); - self.draw_plan_modified = false; + self.draw_call_modified = false; self.draw_commands.commands.clear(); self.text_renderer.reset_frame(); } @@ -90,18 +90,18 @@ impl UiInstance { if self.draw_commands.commands == self.prev_draw_commands.commands { return } - self.draw_plan = UiDrawPlan::build(&self.draw_commands, &mut self.text_renderer); - self.draw_plan_modified = true; + self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.text_renderer); + self.draw_call_modified = true; } - /// Get the draw plan (a list of draw calls) for the current frame + /// Get the draw call for the current frame /// /// This function should only be used by the render backend.\ /// You should not call this directly unless you're implementing a custom render backend /// /// Returns a tuple with a boolean indicating if the draw plan was modified since the last frame - pub fn draw_plan(&self) -> (bool, &UiDrawPlan) { - (self.draw_plan_modified, &self.draw_plan) + pub fn draw_call(&self) -> (bool, &UiDrawCall) { + (self.draw_call_modified, &self.draw_call) } /// Get the font texture for the current frame