diff --git a/src/lib.rs b/src/lib.rs index 643fff0..235b4b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,6 +288,18 @@ impl RendererConfig<'_> { } } +pub struct RenderData { + fb_size: [f32; 2], + last_size: [f32; 2], + last_pos: [f32; 2], + vertex_buffer: Option, + vertex_buffer_size: usize, + index_buffer: Option, + index_buffer_size: usize, + draw_list_offsets: SmallVec<[(i32, u32); 4]>, + render: bool, +} + pub struct Renderer { pipeline: RenderPipeline, uniform_buffer: Buffer, @@ -295,8 +307,7 @@ pub struct Renderer { /// Textures of the font atlas and all images. pub textures: Textures, texture_layout: BindGroupLayout, - index_buffers: SmallVec<[Buffer; 4]>, - vertex_buffers: SmallVec<[Buffer; 4]>, + render_data: Option, config: RendererConfig<'static>, } @@ -447,8 +458,7 @@ impl Renderer { uniform_bind_group, textures: Textures::new(), texture_layout, - vertex_buffers: SmallVec::new(), - index_buffers: SmallVec::new(), + render_data: None, config: RendererConfig { texture_format, depth_format, @@ -465,72 +475,181 @@ impl Renderer { renderer } - /// Render the current imgui frame. - pub fn render<'r>( - &'r mut self, + /// Prepares buffers for the current imgui frame. This must be + /// called before `Renderer::split_render`, and its output must + /// be passed to the render call. + pub fn prepare( + &self, draw_data: &DrawData, + render_data: Option, queue: &Queue, device: &Device, - rpass: &mut RenderPass<'r>, - ) -> RendererResult<()> { + ) -> RenderData { let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0]; let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1]; + let mut render_data = render_data.unwrap_or_else(|| RenderData { + fb_size: [fb_width, fb_height], + last_size: [0.0, 0.0], + last_pos: [0.0, 0.0], + vertex_buffer: None, + vertex_buffer_size: 0, + index_buffer: None, + index_buffer_size: 0, + draw_list_offsets: SmallVec::<[_; 4]>::new(), + render: false, + }); + // If the render area is <= 0, exit here and now. - if !(fb_width > 0.0 && fb_height > 0.0) { - return Ok(()); + if fb_width <= 0.0 || fb_height <= 0.0 { + render_data.render = false; + return render_data; + } else { + render_data.render = true; } - let width = draw_data.display_size[0]; - let height = draw_data.display_size[1]; - - let offset_x = draw_data.display_pos[0] / width; - let offset_y = draw_data.display_pos[1] / height; - - // Create and update the transform matrix for the current frame. - // This is required to adapt to vulkan coordinates. - // let matrix = [ - // [2.0 / width, 0.0, 0.0, 0.0], - // [0.0, 2.0 / height as f32, 0.0, 0.0], - // [0.0, 0.0, -1.0, 0.0], - // [-1.0, -1.0, 0.0, 1.0], - // ]; - let matrix = [ - [2.0 / width, 0.0, 0.0, 0.0], - [0.0, 2.0 / -height as f32, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [-1.0 - offset_x * 2.0, 1.0 + offset_y * 2.0, 0.0, 1.0], - ]; - self.update_uniform_buffer(queue, &matrix); + // Only update matrices if the size or position changes + if (render_data.last_size[0] - draw_data.display_size[0]).abs() > std::f32::EPSILON + || (render_data.last_size[1] - draw_data.display_size[1]).abs() > std::f32::EPSILON + || (render_data.last_pos[0] - draw_data.display_pos[0]).abs() > std::f32::EPSILON + || (render_data.last_pos[1] - draw_data.display_pos[1]).abs() > std::f32::EPSILON + { + render_data.last_size = draw_data.display_size; + render_data.last_pos = draw_data.display_pos; + + let width = draw_data.display_size[0]; + let height = draw_data.display_size[1]; + + let offset_x = draw_data.display_pos[0] / width; + let offset_y = draw_data.display_pos[1] / height; + + // Create and update the transform matrix for the current frame. + // This is required to adapt to vulkan coordinates. + let matrix = [ + [2.0 / width, 0.0, 0.0, 0.0], + [0.0, 2.0 / -height as f32, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [-1.0 - offset_x * 2.0, 1.0 + offset_y * 2.0, 0.0, 1.0], + ]; + self.update_uniform_buffer(queue, &matrix); + } - rpass.set_pipeline(&self.pipeline); - rpass.set_bind_group(0, &self.uniform_bind_group, &[]); + render_data.draw_list_offsets.clear(); - self.vertex_buffers.clear(); - self.index_buffers.clear(); + let mut vertex_count = 0; + let mut index_count = 0; + for draw_list in draw_data.draw_lists() { + render_data + .draw_list_offsets + .push((vertex_count as i32, index_count as u32)); + vertex_count += draw_list.vtx_buffer().len(); + index_count += draw_list.idx_buffer().len(); + } + + let mut vertices = Vec::with_capacity(vertex_count * std::mem::size_of::()); + let mut indices = Vec::with_capacity(index_count * std::mem::size_of::()); for draw_list in draw_data.draw_lists() { - self.vertex_buffers - .push(self.upload_vertex_buffer(device, draw_list.vtx_buffer())); - self.index_buffers - .push(self.upload_index_buffer(device, draw_list.idx_buffer())); + // Safety: DrawVertPod is #[repr(transparent)] over DrawVert and DrawVert _should_ be Pod. + let vertices_pod: &[DrawVertPod] = unsafe { draw_list.transmute_vtx_buffer() }; + vertices.extend_from_slice(bytemuck::cast_slice(vertices_pod)); + indices.extend_from_slice(bytemuck::cast_slice(draw_list.idx_buffer())); + } + + // Copies in wgpu must be padded to 4 byte alignment + indices.resize( + indices.len() + COPY_BUFFER_ALIGNMENT as usize + - indices.len() % COPY_BUFFER_ALIGNMENT as usize, + 0, + ); + + // If the buffer is not created or is too small for the new indices, create a new buffer + if render_data.index_buffer.is_none() || render_data.index_buffer_size < indices.len() { + let buffer = device.create_buffer_init(&BufferInitDescriptor { + label: Some("imgui-wgpu index buffer"), + contents: &indices, + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + }); + render_data.index_buffer = Some(buffer); + render_data.index_buffer_size = indices.len(); + } else if let Some(buffer) = render_data.index_buffer.as_ref() { + // The buffer is large enough for the new indices, so reuse it + queue.write_buffer(buffer, 0, &indices); + } else { + unreachable!() + } + + // If the buffer is not created or is too small for the new vertices, create a new buffer + if render_data.vertex_buffer.is_none() || render_data.vertex_buffer_size < vertices.len() { + let buffer = device.create_buffer_init(&BufferInitDescriptor { + label: Some("imgui-wgpu vertex buffer"), + contents: &vertices, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + }); + render_data.vertex_buffer = Some(buffer); + render_data.vertex_buffer_size = vertices.len(); + } else if let Some(buffer) = render_data.vertex_buffer.as_ref() { + // The buffer is large enough for the new vertices, so reuse it + queue.write_buffer(buffer, 0, &vertices); + } else { + unreachable!() + } + + render_data + } + + /// Render the current imgui frame. `Renderer::prepare` must be + /// called first, and the output render data must be kept for the + /// lifetime of the renderpass. + pub fn split_render<'r>( + &'r self, + draw_data: &DrawData, + render_data: &'r RenderData, + rpass: &mut RenderPass<'r>, + ) -> RendererResult<()> { + if !render_data.render { + return Ok(()); } + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, &self.uniform_bind_group, &[]); + rpass.set_vertex_buffer(0, render_data.vertex_buffer.as_ref().unwrap().slice(..)); + rpass.set_index_buffer( + render_data.index_buffer.as_ref().unwrap().slice(..), + IndexFormat::Uint16, + ); + // Execute all the imgui render work. - for (draw_list_buffers_index, draw_list) in draw_data.draw_lists().enumerate() { + for (draw_list, bases) in draw_data + .draw_lists() + .zip(render_data.draw_list_offsets.iter()) + { self.render_draw_list( rpass, draw_list, - [fb_width, fb_height], + render_data.fb_size, draw_data.display_pos, draw_data.framebuffer_scale, - draw_list_buffers_index, + *bases, )?; } Ok(()) } + /// Render the current imgui frame. + pub fn render<'r>( + &'r mut self, + draw_data: &DrawData, + queue: &Queue, + device: &Device, + rpass: &mut RenderPass<'r>, + ) -> RendererResult<()> { + let render_data = self.render_data.take(); + self.render_data = Some(self.prepare(draw_data, render_data, queue, device)); + self.split_render(draw_data, self.render_data.as_ref().unwrap(), rpass) + } + /// Render a given `DrawList` from imgui onto a wgpu frame. fn render_draw_list<'render>( &'render self, @@ -539,16 +658,9 @@ impl Renderer { fb_size: [f32; 2], clip_off: [f32; 2], clip_scale: [f32; 2], - draw_list_buffers_index: usize, + (vertex_base, index_base): (i32, u32), ) -> RendererResult<()> { - let mut start = 0; - - let index_buffer = &self.index_buffers[draw_list_buffers_index]; - let vertex_buffer = &self.vertex_buffers[draw_list_buffers_index]; - - // Make sure the current buffers are attached to the render pass. - rpass.set_index_buffer(index_buffer.slice(..), IndexFormat::Uint16); - rpass.set_vertex_buffer(0, vertex_buffer.slice(..)); + let mut start = index_base; for cmd in draw_list.commands() { if let Elements { count, cmd_params } = cmd { @@ -590,7 +702,7 @@ impl Renderer { rpass.set_scissor_rect(scissors.0, scissors.1, scissors.2, scissors.3); // Draw the current batch of vertices with the renderpass. - rpass.draw_indexed(start..end, 0, 0..1); + rpass.draw_indexed(start..end, vertex_base, 0..1); } } @@ -603,38 +715,11 @@ impl Renderer { } /// Updates the current uniform buffer containing the transform matrix. - fn update_uniform_buffer(&mut self, queue: &Queue, matrix: &[[f32; 4]; 4]) { + fn update_uniform_buffer(&self, queue: &Queue, matrix: &[[f32; 4]; 4]) { let data = bytemuck::bytes_of(matrix); - queue.write_buffer(&self.uniform_buffer, 0, data); } - /// Upload the vertex buffer to the GPU. - fn upload_vertex_buffer(&self, device: &Device, vertices: &[DrawVert]) -> Buffer { - // Safety: DrawVertPod is #[repr(transparent)] over DrawVert and DrawVert _should_ be Pod. - let vertices = unsafe { - std::slice::from_raw_parts(vertices.as_ptr() as *mut DrawVertPod, vertices.len()) - }; - - let data = bytemuck::cast_slice(vertices); - device.create_buffer_init(&BufferInitDescriptor { - label: Some("imgui-wgpu vertex buffer"), - contents: data, - usage: BufferUsages::VERTEX, - }) - } - - /// Upload the index buffer to the GPU. - fn upload_index_buffer(&self, device: &Device, indices: &[DrawIdx]) -> Buffer { - let data = bytemuck::cast_slice(indices); - - device.create_buffer_init(&BufferInitDescriptor { - label: Some("imgui-wgpu index buffer"), - contents: data, - usage: BufferUsages::INDEX, - }) - } - /// Updates the texture on the GPU corresponding to the current imgui font atlas. /// /// This has to be called after loading a font.