tirbofish/dropbear · commit
ed895cea6815c8c50b4b6ef341e712060c066cb8
feature: kino-ui works
Signature present but could not be verified.
Unverified
@@ -197,6 +197,7 @@ impl InstanceRaw { /// A wrapper to the [wgpu::CommandEncoder] pub struct CommandEncoder { + queue: Arc<Queue>, inner: wgpu::CommandEncoder, } @@ -218,13 +219,14 @@ impl CommandEncoder { /// Creates a new instance of a command encoder. pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self { Self { + queue: graphics.queue.clone(), inner: graphics.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }), } } - - /// Submits the command encoder for execution. - /// - /// Panics if an unwinding error is caught, or just returns the error as normal. + + /// Submits the command encoder for execution. + /// + /// Panics if an unwinding error is caught, or just returns the error as normal. pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { let command_buffer = self.inner.finish(); @@ -11,6 +11,7 @@ readme = "README.md" eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] } magna-carta = { path = "../magna-carta" } redback-runtime = { path = "../redback-runtime", features = ["debug"]} +kino-ui = { path = "../kino-ui" } anyhow.workspace = true app_dirs2.workspace = true @@ -54,7 +55,6 @@ rfd.workspace = true semver.workspace = true serde.workspace = true - [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -101,6 +101,7 @@ async fn main() -> anyhow::Result<()> { ) .filter(Some("eucalyptus_core"), LevelFilter::Debug) .filter(Some("dropbear_traits"), LevelFilter::Debug) + .filter(Some("kino_ui"), LevelFilter::Trace) .init(); log::info!("Initialised logger"); } @@ -0,0 +1,17 @@ +[package] +name = "kino-ui" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +authors.workspace = true + +[dependencies] +wgpu.workspace = true +parking_lot.workspace = true +glam.workspace = true +bytemuck.workspace = true +image.workspace = true +anyhow.workspace = true +log.workspace = true @@ -0,0 +1,51 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use crate::rendering::texture::Texture; + +/// A handle with type [`T`] that provides an index to the [AssetServer] contents. +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] +pub struct Handle<T> { + pub id: u64, + _phanton: PhantomData<T> +} + +impl<T> Handle<T> { + pub fn new(id: u64) -> Self { + Self { id, _phanton: Default::default() } + } +} + +/// Serves assets as a [Handle], allowing you to reference any object. +#[derive(Default)] +pub struct AssetServer { + textures: HashMap<u64, Texture>, +} + +impl AssetServer { + /// Creates a new instance of an AssetServer. + pub fn new() -> AssetServer { + AssetServer { + textures: Default::default(), + } + } + + /// Adds a texture and returns a handle. + /// + /// This assumes a [Texture] has already been created by you. To create a new texture, + /// you can use [`Texture::from_bytes`]. + pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> { + let handle = Handle::new(texture.hash); + self.textures.insert(handle.id, texture); + handle + } + + /// Updates the asset server by inserting the texture provided at the location of the handle, + /// and removing the old texture (by returning it back to you). + pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> { + self.textures.insert(handle.id, texture) + } + + pub fn get_texture(&mut self, handle: Handle<Texture>) -> Option<&Texture> { + self.textures.get(&handle.id) + } +} @@ -0,0 +1,74 @@ +use bytemuck::{Pod, Zeroable}; +use glam::{Mat4, Vec2}; +use crate::math::Rect; + +pub struct Camera2D { + position: Vec2, + zoom: f32, +} + +impl Default for Camera2D { + fn default() -> Self { + Self { + position: Vec2::ZERO, + zoom: 1.0, + } + } +} + +impl Camera2D { + /// Returns the orthographic view-projection matrix for the current camera state + pub(crate) fn view_proj(&self, screen_size: Vec2) -> Mat4 { + let width = screen_size.x / self.zoom; + let height = screen_size.y / self.zoom; + + let left = self.position.x; + let right = self.position.x + width; + let top = self.position.y; + let bottom = self.position.y + height; + + Mat4::orthographic_lh(left, right, bottom, top, -1.0, 1.0) + } + + /// Set the camera's position (top-left corner of view) + pub fn target(&mut self, position: Vec2) { + self.position = position; + } + + /// Center the camera on a position + pub fn center(&mut self, position: Vec2, screen_size: Vec2) { + self.position = position - screen_size / (2.0 * self.zoom); + } + + /// Set zoom level, clamped between 0.1 & 10.0 to avoid insanity + pub fn set_zoom(&mut self, zoom: f32) { + self.zoom = zoom.clamp(0.1, 10.0); + } + + /// Returns the viewport rectangle in world coordinates, factoring in zoom + /// Useful for culling or visibility checks + pub fn viewport(&self, screen_size: Vec2) -> Rect { + let size = screen_size / self.zoom; + Rect::new(self.position, size) + } + /// Converts a point from world space to screen space (pixels) + pub fn world_to_screen(&self, world: Vec2) -> Vec2 { + (world - self.position) * self.zoom + } + + /// Converts a point from screen space back to world space + pub fn screen_to_world(&self, screen: Vec2) -> Vec2 { + screen / self.zoom + self.position + } +} + +pub struct CameraRendering { + pub buffer: wgpu::Buffer, + pub bind_group: wgpu::BindGroup, +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct CameraUniform { + pub view_proj: [[f32; 4]; 4], +} @@ -0,0 +1,258 @@ +//! kino-ui, a UI library with instruction set UI rendering. +//! +//! Uses wgpu for rendering and winit for input management. + +pub mod resp; +pub mod widgets; +pub mod rendering; +pub mod camera; +pub mod asset; +pub mod math; + +use crate::asset::{AssetServer, Handle}; +use crate::camera::Camera2D; +use crate::rendering::texture::Texture; +use crate::rendering::vertex::Vertex; +use crate::rendering::{KinoWGPURenderer, VertexBatch}; +use crate::resp::WidgetResponse; +use crate::widgets::{ContaineredWidget, NativeWidget}; +use std::collections::{HashMap, VecDeque}; +use std::fmt::Debug; +use std::hash::{DefaultHasher, Hash, Hasher}; +use wgpu::{LoadOp, StoreOp}; + +/// Holds the state of all the instructions, and the vertices+indices for rendering as well +/// as the responses. +pub struct KinoState { + renderer: KinoWGPURenderer, + instruction_set: VecDeque<UiInstructionType>, + widget_states: HashMap<WidgetId, WidgetResponse>, + assets: AssetServer, + batch: PrimitiveBatch, + camera: Camera2D, +} + +impl KinoState { + /// Creates a new instance of a [KinoState]. + /// + /// This sits inside your `init()` function. + pub fn new(renderer: KinoWGPURenderer) -> KinoState { + log::debug!("Created KinoState"); + KinoState { + renderer, + instruction_set: Default::default(), + widget_states: Default::default(), + assets: Default::default(), + batch: Default::default(), + camera: Camera2D::default(), + } + } + + pub fn add_widget(&mut self, widget: Box<dyn NativeWidget>) { + self.instruction_set.push_back(UiInstructionType::Widget(widget)); + } + + pub fn add_container(&mut self, _container: Box<dyn ContaineredWidget>) { + todo!("This is broken rn and idk how to implement it") + // self.instruction_set.push_back(UiInstructionType::Containered( + // ContaineredWidgetType::Start { + // id: container.id(), + // widget: container, + // } + // )) + } + + pub fn add_instruction(&mut self, ui_instruction_type: UiInstructionType) { + self.instruction_set.push_back(ui_instruction_type); + } + + /// Polls for changes, builds the tree and prepares them for rendering. + /// + /// This sits inside your `update()` loop. + pub fn poll(&mut self) { + log::trace!("polling kinostate"); + let current_instructions = { + self.instruction_set.drain(..).collect::<Vec<_>>() + }; + + self.widget_states.clear(); + + let tree = Self::build_tree(current_instructions); + + self.render_tree(tree); + } + + /// Pushes the vertices and indices to the renderer. + /// + /// This sits inside your `render()` loop. + pub fn render( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + view: &wgpu::TextureView, + ) { + log::trace!("rendering kinostate"); + self.renderer.upload_camera_matrix( + queue, + self.camera + .view_proj(self.renderer.size) + .to_cols_array_2d(), + ); + let batch = self.batch.take(); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("kino render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + for mut tg in batch { + log::debug!("Rendering textured geometry: {:?}", tg); + let texture = tg.texture_id.and_then(|v| { + self.assets.get_texture(v) + }); + self.renderer.draw_batch(&mut pass, device, queue, &mut tg.batch, texture); + } + } + } + + fn build_tree(instructions: Vec<UiInstructionType>) -> Vec<UiNode> { + let mut stack: Vec<UiNode> = Vec::new(); + let mut root = Vec::new(); + + for instruction in instructions { + match &instruction { + UiInstructionType::Containered(container_ty) => { + match container_ty { + ContaineredWidgetType::Start { .. } => { + stack.push(UiNode { + instruction, + children: Vec::new(), + }); + } + ContaineredWidgetType::End { .. } => { + if let Some(node) = stack.pop() { + if let Some(parent) = stack.last_mut() { + parent.children.push(node); + } else { + root.push(node); + } + } + } + } + } + UiInstructionType::Widget(_) => { + let node = UiNode { + instruction, + children: Vec::new(), + }; + + if let Some(parent) = stack.last_mut() { + parent.children.push(node); + } else { + root.push(node); + } + } + } + } + + root + } + + fn render_tree(&mut self, nodes: Vec<UiNode>) { + for node in nodes { + match node.instruction { + UiInstructionType::Containered(container_ty) => { + match container_ty { + ContaineredWidgetType::Start { widget, .. } => { + log::trace!("Rendering containered widget START"); + widget.render(node.children, self); + } + ContaineredWidgetType::End { .. } => { + log::trace!("Rendering end widget END"); + // already handled in tree building + } + } + } + UiInstructionType::Widget(widget) => { + log::trace!("Rendering widget: {:?}", widget.id()); + widget.render(self); + } + } + } + } +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] +pub struct WidgetId(pub u64); + +impl Into<WidgetId> for &str { + fn into(self) -> WidgetId { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + WidgetId(hasher.finish()) + } +} + +pub enum UiInstructionType { + Containered(ContaineredWidgetType), + Widget(Box<dyn NativeWidget>), +} + +pub enum ContaineredWidgetType { + Start { + id: WidgetId, + widget: Box<dyn ContaineredWidget>, + }, + End { + id: WidgetId, + } +} + +pub struct UiNode { + pub instruction: UiInstructionType, + pub children: Vec<UiNode>, +} + +#[derive(Debug, Default)] +pub struct TexturedGeometry { + pub texture_id: Option<Handle<Texture>>, + pub batch: VertexBatch, +} + +#[derive(Default)] +pub struct PrimitiveBatch { + geometry: Vec<TexturedGeometry>, +} + +impl PrimitiveBatch { + /// Add verts & indices to batch, preserving submission order & batching consecutive geometry per texture + pub fn push(&mut self, verts: &[Vertex], indices: &[u16], texture_id: Option<Handle<Texture>>) { + if let Some(tg) = self.geometry.last_mut() + && tg.texture_id == texture_id + { + tg.batch.push(verts, indices); + return; + } + + let mut batch = VertexBatch::default(); + batch.push(verts, indices); + self.geometry.push(TexturedGeometry { texture_id, batch }); + } + + pub(crate) fn take(&mut self) -> Vec<TexturedGeometry> { + std::mem::take(&mut self.geometry) + } +} @@ -0,0 +1,48 @@ +use glam::{vec2, Vec2}; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Rect { + pub position: Vec2, + pub size: Vec2, +} + +impl Rect { + /// Create a new rectangle from position (top-left) & size + pub fn new(position: Vec2, size: Vec2) -> Self { + Self { position, size } + } + + /// Returns the top-left corner (min coords) + pub fn min(&self) -> Vec2 { + self.position + } + + /// Returns the bottom-right corner (max coords) + pub fn max(&self) -> Vec2 { + self.position + self.size + } + + /// Returns the center point of the rectangle + pub fn center(&self) -> Vec2 { + self.position + self.size * 0.5 + } + + // Move the rectangle by the given delta vector + pub fn translate(&mut self, delta: Vec2) { + self.position += delta; + } + + /// Returns true if the point is inside of the rectangle + pub fn contains(&self, point: Vec2) -> bool { + point.cmpge(self.position).all() && point.cmple(self.position + self.size).all() + } + + /// Returns the four corners in this order: top-left, top-right, bottom-right, bottom-left + pub fn corners(&self) -> [Vec2; 4] { + let tl = self.position; + let tr = vec2(tl.x + self.size.x, tl.y); + let br = vec2(tl.x + self.size.x, tl.y + self.size.y); + let bl = vec2(tl.x, tl.y + self.size.y); + [tl, tr, br, bl] + } +} @@ -0,0 +1,215 @@ +use glam::Vec2; +use wgpu::{BufferUsages, IndexFormat}; +use wgpu::util::{BufferInitDescriptor, DeviceExt}; +use crate::camera::{CameraRendering, CameraUniform}; +use crate::rendering::pipeline::KinoRendererPipeline; +use crate::rendering::vertex::Vertex; + +pub mod pipeline; +pub mod texture; +pub mod vertex; + +pub struct KinoWGPURenderer { + pipeline: KinoRendererPipeline, + default_texture: texture::Texture, + pub size: Vec2, + + camera: CameraRendering, +} + +impl KinoWGPURenderer { + /// Creates a new `wgpu` renderer for the kino ui system. + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + surface_format: wgpu::TextureFormat, + size: [f32; 2], + ) -> Self { + log::debug!("Creating KinoWGPURenderer"); + let pipeline = KinoRendererPipeline::new(device, surface_format); + + let camera_buffer = device.create_buffer_init(&BufferInitDescriptor { + label: None, + contents: bytemuck::bytes_of(&CameraUniform { + view_proj: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pipeline.camera_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout); + log::debug!("Created KinoWGPURenderer"); + Self { + pipeline, + default_texture, + size: Vec2::from_array(size), + camera: CameraRendering { + buffer: camera_buffer, + bind_group: camera_bind_group, + }, + } + } + + pub fn upload_camera_matrix(&mut self, queue: &wgpu::Queue, view_proj: [[f32; 4]; 4]) { + queue.write_buffer( + &self.camera.buffer, + 0, + bytemuck::bytes_of(&CameraUniform { view_proj }), + ); + } + + pub fn draw_batch( + &self, + r_pass: &mut wgpu::RenderPass<'_>, + device: &wgpu::Device, + queue: &wgpu::Queue, + batch: &mut VertexBatch, + texture: Option<&texture::Texture>, + ) { + if batch.is_empty() { + return; + } + batch.upload(&device, &queue); + + let texture = texture.unwrap_or(&self.default_texture); + + texture.bind(r_pass, 0); + + r_pass.set_pipeline(&self.pipeline.pipeline); + r_pass.set_bind_group(1, &self.camera.bind_group, &[]); + + batch.draw(r_pass); + batch.clear(); + } +} + +/// Describes a primitive shape. +#[derive(Debug)] +pub struct VertexBatch { + vertices: Vec<Vertex>, + indices: Vec<u16>, + vertex_buffer: Option<wgpu::Buffer>, + index_buffer: Option<wgpu::Buffer>, + vertices_dirty: bool, + indices_dirty: bool, +} + +impl Default for VertexBatch { + fn default() -> Self { + Self { + vertices: Vec::with_capacity(Self::MAX_VERTICES), + indices: Vec::with_capacity(Self::MAX_INDICES), + vertex_buffer: None, + index_buffer: None, + vertices_dirty: false, + indices_dirty: false, + } + } +} + +impl VertexBatch { + const MAX_VERTICES: usize = u16::MAX as usize; + const MAX_INDICES: usize = Self::MAX_VERTICES * 6; + + /// Returns true if adding verts/indices would exceed max allowed + fn would_overflow(&self, vert_count: usize, idx_count: usize) -> bool { + self.vertices.len() + vert_count > Self::MAX_VERTICES + || self.indices.len() + idx_count > Self::MAX_INDICES + } + + /// Adds vertices/indices, returns false if it would overflow + pub fn push(&mut self, verts: &[Vertex], indices: &[u16]) -> bool { + if self.would_overflow(verts.len(), indices.len()) { + return false; + } + + let idx_offset = self.vertices.len() as u16; + self.vertices.extend_from_slice(verts); + self.indices.extend(indices.iter().map(|i| *i + idx_offset)); + + self.vertices_dirty = true; + self.indices_dirty = true; + + true + } + + fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + if self.is_empty() || (!self.vertices_dirty && !self.indices_dirty) { + return; + } + + if self.vertex_buffer.is_none() { + self.vertex_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: Some("kino vertex buffer"), + size: (Self::MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64, + usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + } + if self.index_buffer.is_none() { + self.index_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: Some("kino index buffer"), + size: (Self::MAX_INDICES * std::mem::size_of::<u16>()) as u64, + usage: BufferUsages::INDEX | BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + } + + if self.vertices_dirty { + queue.write_buffer( + self.vertex_buffer.as_ref().unwrap(), + 0, + bytemuck::cast_slice(&self.vertices), + ); + self.vertices_dirty = false; + } + if self.indices_dirty { + let mut indices_bytes: Vec<u8> = bytemuck::cast_slice(&self.indices).to_vec(); + let remainder = indices_bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize; + if remainder != 0 { + let pad_len = wgpu::COPY_BUFFER_ALIGNMENT as usize - remainder; + indices_bytes.extend_from_slice(&vec![0u8; pad_len]); + } + + queue.write_buffer(self.index_buffer.as_ref().unwrap(), 0, &indices_bytes); + self.indices_dirty = false; + } + } + + fn is_empty(&self) -> bool { + self.vertices.is_empty() || self.indices.is_empty() + } + + fn clear(&mut self) { + self.vertices.clear(); + self.indices.clear(); + self.vertices_dirty = true; + self.indices_dirty = true; + } + + fn draw(&self, pass: &mut wgpu::RenderPass) { + if self.is_empty() { + return; + } + + pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); + pass.set_index_buffer( + self.index_buffer.as_ref().unwrap().slice(..), + IndexFormat::Uint16, + ); + pass.draw_indexed(0..self.indices.len() as u32, 0, 0..1); + } +} @@ -0,0 +1,113 @@ +use crate::rendering::vertex::Vertex; + +pub struct KinoRendererPipeline { + pub(crate) pipeline: wgpu::RenderPipeline, + pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout, + pub(crate) camera_bind_group_layout: wgpu::BindGroupLayout, +} + +impl KinoRendererPipeline { + pub fn new( + device: &wgpu::Device, + surface_format: wgpu::TextureFormat, + ) -> Self { + log::debug!("Initialising pipeline with format {:?}", surface_format); + let shader = device.create_shader_module(wgpu::include_wgsl!("../shaders/ui.wgsl")); + + let texture_bind_group_layout = Self::create_texture_bind_group_layout(device); + let camera_bind_group_layout = Self::create_camera_bind_group_layout(device); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("kino pipeline layout descriptor"), + bind_group_layouts: &[ + &texture_bind_group_layout, + &camera_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("kino render pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + log::debug!("Created pipeline"); + + Self { + pipeline, + texture_bind_group_layout, + camera_bind_group_layout, + } + } + + fn create_texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("kino texture bind group layout"), + entries: &[ + // texture binding + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + + // texture sampler + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }) + } + + fn create_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("kino camera bind group layout"), + entries: &[ + // camera uniform + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + } + ], + }) + } +} @@ -0,0 +1,108 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; +use wgpu::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource, Device, + Extent3d, Origin3d, Queue, RenderPass, TexelCopyBufferLayout, TexelCopyTextureInfo, + TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, +}; + +/// A GPU texture that can be bound in shaders for rendering +/// +/// Wraps a `wgpu::Texture`, its view, sampler, & bind group +#[derive(Debug, PartialEq)] +pub struct Texture { + pub(crate) hash: u64, + bind_group: BindGroup, +} + +impl Texture { + /// Creates a new texture from raw RGBA image data, + /// uploads the data, & builds the bind group using the layout + /// + /// - `data`: Must be in tightly packed 8-bit RGBA format + /// - `width`, `height`: Dimensions of the image in pixels + pub fn from_bytes( + device: &Device, + queue: &Queue, + bind_group_layout: &BindGroupLayout, + data: &[u8], + width: u32, + height: u32, + ) -> Self { + log::debug!("Creating new texture"); + + let texture = device.create_texture(&TextureDescriptor { + label: None, + size: Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + + queue.write_texture( + TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + data, + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width), + rows_per_image: Some(height), + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + let view = texture.create_view(&Default::default()); + let sampler = device.create_sampler(&Default::default()); + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: None, + layout: bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + let hash = hasher.finish(); + + log::debug!("Created new texture [{}]", hash); + + Self { hash, bind_group } + } + + /// Creates a 1×1 white fallback texture + /// + /// Used when no valid texture is provided for a draw call + pub fn create_default(device: &Device, queue: &Queue, layout: &BindGroupLayout) -> Self { + log::debug!("Creating standard white texture"); + Self::from_bytes(device, queue, layout, &[255u8, 255, 255, 255], 1, 1) + } + + /// Binds this texture at the given index in the render pass + /// + /// - `index` must match the bind group index used in the pipeline layout + pub fn bind(&self, pass: &mut RenderPass, index: u32) { + pass.set_bind_group(index, &self.bind_group, &[]); + } +} @@ -0,0 +1,48 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable, Debug)] +pub struct Vertex { + pub position: [f32; 2], + pub colour: [f32; 4], + pub tex_coord: [f32; 2], +} + +impl Vertex { + pub fn new(position: impl Into<[f32; 2]>, colour: impl Into<[f32; 4]>, tex_coord: impl Into<[f32; 2]>) -> Self { + Self { + position: position.into(), + colour: colour.into(), + tex_coord: tex_coord.into(), + } + } + + pub fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: size_of::<Vertex>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + // position + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }, + + // colour + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x4, + offset: size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + }, + + // tex_coords + wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: size_of::<[f32; 6]>() as wgpu::BufferAddress, + shader_location: 2, + } + ], + } + } +} @@ -0,0 +1,6 @@ +#[derive(Clone, Copy, Debug, Default)] +pub struct WidgetResponse { + pub clicked: bool, + pub hovering: bool, + pub checked: bool, +} @@ -0,0 +1,38 @@ +@group(0) @binding(0) +var texture_binding: texture_2d<f32>; + +@group(0) @binding(1) +var texture_sampler: sampler; + +struct CameraUniform { + view_proj: mat4x4<f32>, +}; + +@group(1) @binding(0) +var<uniform> camera: CameraUniform; + +struct VertexInput { + @location(0) position: vec2<f32>, + @location(1) colour: vec4<f32>, + @location(2) tex_coords: vec2<f32>, +}; + +struct VertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) colour: vec4<f32>, + @location(1) tex_coords: vec2<f32>, +}; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = camera.view_proj * vec4<f32>(input.position, 0.0, 1.0); + output.colour = input.colour; + output.tex_coords = input.tex_coords; + return output; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { + return textureSample(texture_binding, texture_sampler, input.tex_coords) * input.colour; +} @@ -0,0 +1,21 @@ +pub mod rect; + +use std::any::Any; +use crate::{KinoState, UiNode, WidgetId}; + +pub enum Anchor { + Center, + TopLeft, +} + +pub trait NativeWidget: Send + Sync { + fn render(self: Box<Self>, state: &mut KinoState); + fn id(&self) -> WidgetId; + fn as_any(&self) -> &dyn Any; +} + +pub trait ContaineredWidget: Send + Sync { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState); + fn id(&self) -> WidgetId; + fn as_any(&self) -> &dyn Any; +} @@ -0,0 +1,128 @@ +use std::any::Any; +use glam::{vec2, Mat2, Vec2}; +use crate::{KinoState, WidgetId}; +use crate::asset::Handle; +use crate::math::Rect; +use crate::rendering::texture::Texture; +use crate::rendering::vertex::Vertex; +use crate::widgets::{Anchor, NativeWidget}; + +pub struct Rectangle { + /// The identifier of the widget. + /// + /// To make life easier, a text id works pretty well. + pub id: WidgetId, + + pub anchor: Anchor, + + /// The position of the rectangle + pub position: Vec2, + + /// The size of the rectangle + pub size: Vec2, + + /// The texture that this + pub texture: Option<Handle<Texture>>, + + /// Colour described as RGBA. + /// + /// If a texture is applied to the colour, it will create a tinted texture on the quad. + pub colour: [f32; 4], // for now until colour is properly implemented + + /// Rotation of the rectangle in radians + pub rotation: f32, + + /// The UV of the textures. + pub uvs: [[f32; 2]; 4], +} + +impl Rectangle { + pub fn new(id: WidgetId) -> Self { + Self { + id, + anchor: Anchor::TopLeft, + position: Vec2::ZERO, + size: vec2(64.0, 64.0), + rotation: 0.0, + colour: [255.0, 255.0, 255.0, 255.0], + uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], + texture: None, + } + } + + /// Sets the position & size from a [`Rect`]. + pub fn with(mut self, rect: &Rect) -> Self { + self.position = rect.position; + self.size = rect.size; + self + } + /// Sets the anchor point of the rectangle + /// Defaults to [`Anchor::TopLeft`]. + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + /// Sets the world-space position of the rectangle + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + /// Sets the size of the rectangle + pub fn size(mut self, size: Vec2) -> Self { + self.size = size; + self + } + /// Sets the color of the rectangle + pub fn color(mut self, colour: [f32; 4]) -> Self { + self.colour = colour; + self + } + /// Sets rotation (in radians) around the rectangle's center + /// 0 radians points up (positive Y), increasing clockwise + pub fn rotate(mut self, angle: f32) -> Self { + self.rotation = angle + std::f32::consts::FRAC_PI_2; + self + } + /// Sets the texture ID for the rectangle + pub fn texture(mut self, id: Handle<Texture>) -> Self { + self.texture = Some(id); + self + } + /// Custom UV coordinates + /// Defaults to covering the full texture ((0,0) - (1,1)) + pub fn uv(mut self, coords: [[f32; 2]; 4]) -> Self { + self.uvs = coords; + self + } +} + +impl NativeWidget for Rectangle { + fn render(self: Box<Self>, state: &mut KinoState) { + let offset = match self.anchor { + Anchor::TopLeft => Vec2::ZERO, + Anchor::Center => -self.size / 2.0, + }; + let top_left = self.position + offset; + let rect = Rect::new(top_left, self.size); + let rot = Mat2::from_angle(self.rotation); + let verts: Vec<_> = rect + .corners() + .iter() + .zip(self.uvs.iter()) + .map(|(&corner, &uv)| { + let world = rot * (corner - rect.center()) + rect.center(); + Vertex::new(world.to_array(), self.colour, uv) + }) + .collect(); + + state.batch.push(&verts, &[0, 1, 2, 2, 3, 0], self.texture); + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} @@ -9,6 +9,7 @@ readme = "README.md" [dependencies] dropbear-engine = { path = "../dropbear-engine" } eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false } +kino-ui = { path = "../kino-ui" } anyhow.workspace = true log.workspace = true @@ -28,9 +28,12 @@ use std::path::PathBuf; use wgpu::SurfaceConfiguration; use winit::window::Fullscreen; use yakui_winit::YakuiWinit; +use dropbear_engine::texture::Texture; use eucalyptus_core::physics::PhysicsState; use eucalyptus_core::rapier3d::prelude::*; use eucalyptus_core::register_components; +use kino_ui::KinoState; +use kino_ui::rendering::KinoWGPURenderer; mod scene; mod input; @@ -146,6 +149,7 @@ pub struct PlayMode { // ui yakui_winit: Option<YakuiWinit>, + kino: Option<kino_ui::KinoState>, } impl PlayMode { @@ -201,6 +205,7 @@ impl PlayMode { last_vsync: true, last_size: (0, 0), }, + kino: None, }; log::debug!("Created new play mode instance"); @@ -213,6 +218,8 @@ impl PlayMode { self.main_pipeline = Some(MainRenderPipeline::new(graphics.clone())); self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals"))); self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); + + self.kino = Some(KinoState::new(KinoWGPURenderer::new(&graphics.device, &graphics.queue, Texture::TEXTURE_FORMAT, [graphics.viewport_texture.size.width as f32, graphics.viewport_texture.size.height as f32]))) } fn reload_scripts_for_current_world(&mut self) { @@ -7,7 +7,7 @@ use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use glam::{DMat4, DQuat, DVec3, Quat}; +use glam::{DMat4, DQuat, DVec3, Quat, Vec2}; use hecs::Entity; use wgpu::{Color}; use wgpu::util::DeviceExt; @@ -35,6 +35,10 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOAD use crate::{PlayMode}; use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; use eucalyptus_core::ui::UI_CONTEXT; +use kino_ui::math::Rect; +use kino_ui::WidgetId; +use kino_ui::widgets::{Anchor, NativeWidget}; +use kino_ui::widgets::rect::Rectangle; impl Scene for PlayMode { fn load(&mut self, graphics: Arc<SharedGraphicsContext>) { @@ -523,21 +527,18 @@ impl Scene for PlayMode { yakui.start(); - eucalyptus_core::ui::poll(); - - // yakui::widgets::Layer::new().show(|| { - // yakui::column(|| { - // let button_response = yakui::widgets::Button::styled("My Button") - // .padding(yakui::widgets::Pad::all(10.0)) - // .show(); - // if button_response.clicked { - // println!("This is clicked!"); - // } - // }); - // }); + // eucalyptus_core::ui::poll(); yakui.finish(); }); + + if let Some(kino) = &mut self.kino { + kino.add_widget(Box::new( + Rectangle::new("rect".into()) + )); + + kino.poll(); + } } else { log::warn!("No such camera exists in the world"); } @@ -913,21 +914,30 @@ impl Scene for PlayMode { log_once::error_once!("{}", e); } - UI_CONTEXT.with(|v| { - let commands = graphics.yakui_renderer.lock().paint( - &mut v.borrow().yakui_state.lock(), - &graphics.device, - &graphics.queue, - SurfaceInfo { - format: Texture::TEXTURE_FORMAT, - sample_count: 1, - color_attachment: &graphics.viewport_texture.view, - resolve_target: None, - } - ); - graphics.queue.submit([commands]); - }); + // UI_CONTEXT.with(|v| { + // let commands = graphics.yakui_renderer.lock().paint( + // &mut v.borrow().yakui_state.lock(), + // &graphics.device, + // &graphics.queue, + // SurfaceInfo { + // format: Texture::TEXTURE_FORMAT, + // sample_count: 1, + // color_attachment: &graphics.viewport_texture.view, + // resolve_target: None, + // } + // ); + // + // graphics.queue.submit([commands]); + // }); + } + + if let Some(kino) = &mut self.kino { + let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder")); + kino.render(&graphics.device, &graphics.queue, &mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit(graphics.clone()) { + log_once::error_once!("Unable to submit kino: {}", e); + } } }