WIP single draw call architecture

This commit is contained in:
griffi-gh 2024-02-21 20:13:58 +01:00
parent 3bcbe0ae6e
commit f54b218cbb
4 changed files with 208 additions and 181 deletions

View file

@ -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<F: Facade>(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<F: Facade>(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::<Vertex>();
let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::<u32>();
@ -106,18 +116,12 @@ impl BufferPair {
}
}
struct GlDrawCall {
active: bool,
buffer: BufferPair,
bind_texture: Option<Rc<SrgbTexture2d>>,
}
pub struct GliumUiRenderer {
context: Rc<Context>,
program: glium::Program,
program_tex: glium::Program,
font_texture: Option<Rc<SrgbTexture2d>>,
plan: Vec<GlDrawCall>,
ui_texture: Option<Rc<SrgbTexture2d>>,
buffer_pair: Option<BufferPair>,
}
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::<Vec<_>>()[..];
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::<Vec<_>>()[..];
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()
}),
},
&params,
).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()
}),
},
&params,
).unwrap();
} else {
frame.draw(
vtx_buffer,
idx_buffer,
&self.program,
&uniform! {
resolution: resolution.to_array(),
},
&params,
).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(),
// },
// &params,
// ).unwrap();
// }
}
}
}

View file

@ -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<UiVertex>,
pub indices: Vec<u32>,
pub bind_texture: Option<BindTexture>,
}
/// Represents a complete UI rendering plan (a list of optimized draw calls).
#[derive(Default)]
pub struct UiDrawPlan {
pub calls: Vec<UiDrawCall>
}
struct CallSwapper {
calls: Vec<UiDrawCall>,
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<UiDrawCall> {
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<UiDrawPlan> for (bool, &UiDrawPlan) {
fn if_modified(&self) -> Option<&UiDrawPlan> {
impl IfModified<UiDrawCall> for (bool, &UiDrawCall) {
fn if_modified(&self) -> Option<&UiDrawCall> {
match self.0 {
true => Some(self.1),
false => None,

106
hui/src/draw/atlas.rs Normal file
View file

@ -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<RefCell<TextureAtlasManager>>,
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<u8>,
allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>,
}
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<TextureHandle> {
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))
}
}

View file

@ -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<UiEvent>,
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<UiEvent>,
}
@ -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