diff --git a/Cargo.lock b/Cargo.lock index 3eadcbe5..21851605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5134,6 +5134,7 @@ dependencies = [ "parking_lot", "png 0.17.16", "pomme-gpu-allocator", + "pomme-gui", "pomme-protocol", "pyronyx", "raw-window-handle", @@ -5169,6 +5170,14 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "pomme-gui" +version = "0.1.0" +dependencies = [ + "tracing", + "winit", +] + [[package]] name = "pomme-launcher" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bd37ed8e..39710917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "pomme-protocol", "tools/blockgen", "tools/protogen" -] +, "pomme-gui"] [workspace.dependencies] serde = { version = "1", features = ["derive"] } @@ -29,6 +29,8 @@ glam = "0.33" pyronyx = "=0.3.2" tracing = "0.1" tracing-subscriber = "0.3" +winit = "0.30" +pomme-gui = { path = "./pomme-gui" } # azalea lags crates.io releases; track the 26.2 branch from git. # azalea-block is replaced by the pomme-block shim (pomme owns block data); diff --git a/pomme-client/Cargo.toml b/pomme-client/Cargo.toml index 38afde6c..a9ce3b68 100644 --- a/pomme-client/Cargo.toml +++ b/pomme-client/Cargo.toml @@ -21,6 +21,7 @@ reqwest = { workspace = true, features = ["blocking"] } pyronyx = { workspace = true, features = ["rwh_06"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +pomme-gui.workspace = true tracing-appender = "0.2" # Per-thread-heap allocator: the chunk-mesh worker pool churns vertex/index Vecs @@ -29,7 +30,7 @@ tracing-appender = "0.2" mimalloc = "0.1" pomme-gpu-allocator = { path = "../pomme-gpu-allocator" } pomme-protocol = { path = "../pomme-protocol" } -winit = { version = "0.30", features = ["rwh_06"] } +winit = { workspace = true, features = ["rwh_06"] } raw-window-handle = "0.6" glam = { workspace = true } clap = { version = "4", features = ["derive"] } diff --git a/pomme-client/build.rs b/pomme-client/build.rs index 0ce7d254..9bc6f74d 100644 --- a/pomme-client/build.rs +++ b/pomme-client/build.rs @@ -87,6 +87,8 @@ fn main() { ("particle.frag", shaderc::ShaderKind::Fragment), ("clouds.vert", shaderc::ShaderKind::Vertex), ("clouds.frag", shaderc::ShaderKind::Fragment), + ("gui_rect.vert", shaderc::ShaderKind::Vertex), + ("gui_rect.frag", shaderc::ShaderKind::Fragment), ]; for (file, kind) in &shaders { diff --git a/pomme-client/src/app/mod.rs b/pomme-client/src/app/mod.rs index 757dde5a..9023d062 100644 --- a/pomme-client/src/app/mod.rs +++ b/pomme-client/src/app/mod.rs @@ -7,6 +7,7 @@ use std::mem::ManuallyDrop; use std::sync::Arc; use std::time::{Duration, Instant}; +use pomme_gui::Gui; use thiserror::Error; use winit::application::ApplicationHandler; use winit::event::{DeviceEvent, DeviceId, WindowEvent}; @@ -208,6 +209,19 @@ impl ApplicationHandler for App { self.core.apply_cursor_grab(&window, None); + let init_gfx = |renderer| { + let gui = + Gui::new(window.inner_size(), self.core.menu.gui_scale_setting, false); + + Gfx { + renderer: ManuallyDrop::new(renderer), + window, + gui, + last_frame: Instant::now(), + fps_counter: FpsCounter::new(), + } + }; + if let Some(server_ip) = quick_access_multiplayer { let connection = spawn_connection( &self.core.tokio_rt, @@ -229,30 +243,16 @@ impl ApplicationHandler for App { self.core.menu.render_distance, ); - let gfx = Gfx { - renderer: ManuallyDrop::new(renderer), - window, - last_frame: Instant::now(), - fps_counter: FpsCounter::new(), - }; - AppPhase::Connecting { - gfx, + gfx: init_gfx(renderer), panorama: Panorama::new(), connect_phase: ConnectionPhase::Connecting, connection, game, } } else { - let gfx = Gfx { - renderer: ManuallyDrop::new(renderer), - window, - last_frame: Instant::now(), - fps_counter: FpsCounter::new(), - }; - AppPhase::InMenu { - gfx, + gfx: init_gfx(renderer), panorama: Panorama::new(), } } @@ -272,8 +272,9 @@ impl ApplicationHandler for App { event_loop.exit(); } WindowEvent::Resized(new_size) => { - if let Some(app_rt) = self.phase.gfx_mut() { - app_rt.renderer.resize(new_size); + if let Some(gfx) = self.phase.gfx_mut() { + gfx.renderer.resize(new_size); + gfx.gui.resize(new_size); } } WindowEvent::ModifiersChanged(mods) => { diff --git a/pomme-client/src/app/phases/connecting.rs b/pomme-client/src/app/phases/connecting.rs index b5805825..4468ff12 100644 --- a/pomme-client/src/app/phases/connecting.rs +++ b/pomme-client/src/app/phases/connecting.rs @@ -1,4 +1,5 @@ use azalea_protocol::packets::game::ServerboundGamePacket; +use pomme_gui::types::Position; use crate::app::core::AppCore; use crate::app::phases::in_game::GameState; @@ -119,6 +120,10 @@ pub fn update_connecting( elements, core.input.cursor_pos(), false, + gfx.gui.extract_render_state(Position::new( + core.input.cursor_pos().0 as i32, + core.input.cursor_pos().1 as i32, + )), ) { tracing::error!("Render error: {e}"); } diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index db406421..59235970 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -2454,6 +2454,11 @@ pub fn update_game( player_preview, book_preview, game.player.eyes_in_water, + gfx.gui + .extract_render_state(pomme_gui::types::Position::new( + core.input.cursor_pos().0 as i32, + core.input.cursor_pos().1 as i32, + )), ) { tracing::error!("Render error: {e}"); } diff --git a/pomme-client/src/app/phases/in_menu.rs b/pomme-client/src/app/phases/in_menu.rs index 1f43ff75..a2c8d408 100644 --- a/pomme-client/src/app/phases/in_menu.rs +++ b/pomme-client/src/app/phases/in_menu.rs @@ -69,6 +69,11 @@ pub fn update_menu( result.elements, core.input.cursor_pos(), core.menu.is_main_screen(), + gfx.gui + .extract_render_state(pomme_gui::types::Position::new( + core.input.cursor_pos().0 as i32, + core.input.cursor_pos().1 as i32, + )), ) { tracing::error!("Render error: {e}"); } diff --git a/pomme-client/src/app/phases/mod.rs b/pomme-client/src/app/phases/mod.rs index 6933f00b..45276bd1 100644 --- a/pomme-client/src/app/phases/mod.rs +++ b/pomme-client/src/app/phases/mod.rs @@ -2,6 +2,7 @@ use std::mem::ManuallyDrop; use std::sync::Arc; use std::time::Instant; +use pomme_gui::Gui; use winit::window::Window; use crate::app::phases::in_game::GameState; @@ -21,6 +22,7 @@ pub struct Gfx { // Window does not require `ManuallyDrop` because it is dropped normally after the renderer by // the compiler. pub window: Arc, + pub gui: Gui, pub last_frame: Instant, pub fps_counter: FpsCounter, } diff --git a/pomme-client/src/renderer/gui_renderer/mod.rs b/pomme-client/src/renderer/gui_renderer/mod.rs new file mode 100644 index 00000000..0de61af2 --- /dev/null +++ b/pomme-client/src/renderer/gui_renderer/mod.rs @@ -0,0 +1,400 @@ +use std::slice; +use std::sync::{Arc, Mutex}; + +use pomme_gpu_allocator::vulkan::{Allocation, Allocator}; +use pomme_gui::graphics::state::{DrawCmd, GuiRenderState, RectCmd}; +use pyronyx::vk; + +use crate::renderer::{shader, util}; + +mod vertex_mode { + #[repr(transparent)] + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + pub struct VertexMode(f32); + impl VertexMode { + pub const RECT: Self = Self(0.0); + } +} +use vertex_mode::VertexMode; + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct Vertex { + pos: [f32; 2], + color: [f32; 4], + mode: VertexMode, + rect_size: [f32; 2], +} + +const MAX_VERTICES: usize = 8192; +const VERTEX_SIZE: usize = size_of::(); + +pub struct GuiRenderer { + pipeline: vk::Pipeline, + pipeline_layout: vk::PipelineLayout, + + globals_layout: vk::DescriptorSetLayout, + descriptor_pool: vk::DescriptorPool, + globals_set: vk::DescriptorSet, + globals_buffer: vk::Buffer, + globals_allocation: Option, + + vertex_buffer: vk::Buffer, + vertex_allocation: Option, +} + +impl GuiRenderer { + pub fn new( + device: &vk::Device, + allocator: &Arc>, + render_pass: vk::RenderPass, + ) -> Self { + let globals_layout = util::create_descriptor_set_layout( + device, + vk::DescriptorType::UniformBuffer, + vk::ShaderStageFlags::Vertex, + ); + + let layout_info = vk::PipelineLayoutCreateInfo { + set_layout_count: 1, + set_layouts: &globals_layout, + ..Default::default() + }; + let pipeline_layout = device + .create_pipeline_layout(&layout_info, None) + .expect("failed to create gui pipeline layout"); + + let pipeline = create_pipeline(device, render_pass, pipeline_layout); + + let pool_sizes = [vk::DescriptorPoolSize { + ty: vk::DescriptorType::UniformBuffer, + descriptor_count: 1, + }]; + let pool_info = vk::DescriptorPoolCreateInfo { + max_sets: 1, + pool_size_count: pool_sizes.len() as u32, + pool_sizes: pool_sizes.as_ptr(), + ..Default::default() + }; + let descriptor_pool = device + .create_descriptor_pool(&pool_info, None) + .expect("failed to create gui descriptor pool"); + + let alloc_info = vk::DescriptorSetAllocateInfo { + descriptor_pool, + descriptor_set_count: 1, + set_layouts: &globals_layout, + ..Default::default() + }; + let mut globals_set = vk::DescriptorSet::null(); + device + .allocate_descriptor_sets(&alloc_info, slice::from_mut(&mut globals_set)) + .expect("failed to allocate gui globals descriptor set"); + + let (globals_buffer, globals_allocation) = + util::create_uniform_buffer(device, allocator, 8, "gui_globals"); + + let buf_info = vk::DescriptorBufferInfo { + buffer: globals_buffer, + offset: 0, + range: 8, + }; + let write = vk::WriteDescriptorSet { + dst_set: globals_set, + dst_binding: 0, + descriptor_count: 1, + descriptor_type: vk::DescriptorType::UniformBuffer, + buffer_info: &buf_info, + ..Default::default() + }; + device.update_descriptor_sets(&[write], &[]); + + let (vertex_buffer, vertex_allocation) = util::create_host_buffer( + device, + allocator, + (MAX_VERTICES * VERTEX_SIZE) as u64, + vk::BufferUsageFlags::VertexBuffer, + "gui_vertices", + ); + + Self { + pipeline, + pipeline_layout, + globals_layout, + descriptor_pool, + globals_set, + globals_buffer, + globals_allocation: Some(globals_allocation), + vertex_buffer, + vertex_allocation: Some(vertex_allocation), + } + } + + /// TODO: once the blur pass exists, `before_blur_cmds` should render into + /// the scene target before the blur runs, and `after_blur_cmds` on top, + /// as two separate calls with the blur pass in between. For now both are + /// concatenated and drawn in one pass, in submission order. + pub fn draw( + &mut self, + cmd: vk::CommandBuffer, + screen_w: f32, + screen_h: f32, + gui_render_state: &mut GuiRenderState, + ) { + let globals: [f32; 2] = [screen_w, screen_h]; + self.globals_allocation + .as_mut() + .unwrap() + .mapped_slice_mut() + .unwrap()[..8] + .copy_from_slice(bytemuck::cast_slice(&globals)); + + let total = + gui_render_state.before_blur_cmds.len() + gui_render_state.after_blur_cmds.len(); + let mut vertices: Vec = Vec::with_capacity(total * 6); + + for draw_cmd in gui_render_state + .before_blur_cmds + .iter() + .chain(gui_render_state.after_blur_cmds.iter()) + { + match draw_cmd { + DrawCmd::Rect(rect) => push_rect(&mut vertices, rect), + } + } + + if vertices.is_empty() { + return; + } + + let count = vertices.len().min(MAX_VERTICES); + if count < vertices.len() { + tracing::warn!( + "GUI vertex buffer overflow, dropping {} vertices", + vertices.len() - count + ); + } + let byte_data = bytemuck::cast_slice(&vertices[..count]); + self.vertex_allocation + .as_mut() + .unwrap() + .mapped_slice_mut() + .unwrap()[..byte_data.len()] + .copy_from_slice(byte_data); + + cmd.bind_pipeline(vk::PipelineBindPoint::Graphics, self.pipeline); + cmd.bind_descriptor_sets( + vk::PipelineBindPoint::Graphics, + self.pipeline_layout, + 0, + &[self.globals_set], + &[], + ); + cmd.bind_vertex_buffers(0, &[self.vertex_buffer], &[0]); + cmd.draw(count as u32, 1, 0, 0); + } + + pub fn recreate_pipeline(&mut self, device: &vk::Device, render_pass: vk::RenderPass) { + device.destroy_pipeline(self.pipeline, None); + self.pipeline = create_pipeline(device, render_pass, self.pipeline_layout); + } + + pub fn destroy(&mut self, device: &vk::Device, allocator: &Arc>) { + let mut alloc = allocator.lock().unwrap(); + + device.destroy_buffer(self.globals_buffer, None); + if let Some(a) = self.globals_allocation.take() { + alloc.free(a).ok(); + } + device.destroy_buffer(self.vertex_buffer, None); + if let Some(a) = self.vertex_allocation.take() { + alloc.free(a).ok(); + } + + drop(alloc); + + device.destroy_pipeline(self.pipeline, None); + device.destroy_pipeline_layout(self.pipeline_layout, None); + device.destroy_descriptor_pool(self.descriptor_pool, None); + device.destroy_descriptor_set_layout(self.globals_layout, None); + } +} + +fn create_pipeline( + device: &vk::Device, + render_pass: vk::RenderPass, + layout: vk::PipelineLayout, +) -> vk::Pipeline { + let vert_spv = shader::include_spirv!("gui_rect.vert.spv"); + let frag_spv = shader::include_spirv!("gui_rect.frag.spv"); + + let vert_module = shader::create_shader_module(device, vert_spv); + let frag_module = shader::create_shader_module(device, frag_spv); + + let stages = [ + vk::PipelineShaderStageCreateInfo { + stage: vk::ShaderStageFlags::Vertex, + module: vert_module, + name: c"main".as_ptr(), + ..Default::default() + }, + vk::PipelineShaderStageCreateInfo { + stage: vk::ShaderStageFlags::Fragment, + module: frag_module, + name: c"main".as_ptr(), + ..Default::default() + }, + ]; + + let binding_descs = [vk::VertexInputBindingDescription { + binding: 0, + stride: VERTEX_SIZE as u32, + input_rate: vk::VertexInputRate::Vertex, + }]; + + let attr_descs = [ + vk::VertexInputAttributeDescription { + location: 0, + binding: 0, + format: vk::Format::R32G32Sfloat, + offset: 0, + }, + vk::VertexInputAttributeDescription { + location: 1, + binding: 0, + format: vk::Format::R32G32B32A32Sfloat, + offset: 8, + }, + vk::VertexInputAttributeDescription { + location: 2, + binding: 0, + format: vk::Format::R32Sfloat, + offset: 24, + }, + vk::VertexInputAttributeDescription { + location: 3, + binding: 0, + format: vk::Format::R32G32Sfloat, + offset: 28, + }, + vk::VertexInputAttributeDescription { + location: 4, + binding: 0, + format: vk::Format::R32Sfloat, + offset: 36, + }, + ]; + + let vertex_input = vk::PipelineVertexInputStateCreateInfo { + vertex_binding_description_count: binding_descs.len() as u32, + vertex_binding_descriptions: binding_descs.as_ptr(), + vertex_attribute_description_count: attr_descs.len() as u32, + vertex_attribute_descriptions: attr_descs.as_ptr(), + ..Default::default() + }; + + let input_assembly = vk::PipelineInputAssemblyStateCreateInfo { + topology: vk::PrimitiveTopology::TriangleList, + ..Default::default() + }; + + let viewport_state = vk::PipelineViewportStateCreateInfo { + viewport_count: 1, + scissor_count: 1, + ..Default::default() + }; + + let rasterizer = vk::PipelineRasterizationStateCreateInfo { + polygon_mode: vk::PolygonMode::Fill, + cull_mode: vk::CullModeFlags::None, + line_width: 1.0, + ..Default::default() + }; + + let multisampling = vk::PipelineMultisampleStateCreateInfo { + rasterization_samples: vk::SampleCountFlags::Type1, + ..Default::default() + }; + + let depth_stencil = vk::PipelineDepthStencilStateCreateInfo { + depth_test_enable: vk::FALSE, + depth_write_enable: vk::FALSE, + ..Default::default() + }; + + let blend_attachment = [vk::PipelineColorBlendAttachmentState { + blend_enable: vk::TRUE, + src_color_blend_factor: vk::BlendFactor::One, + dst_color_blend_factor: vk::BlendFactor::OneMinusSrcAlpha, + color_blend_op: vk::BlendOp::Add, + src_alpha_blend_factor: vk::BlendFactor::One, + dst_alpha_blend_factor: vk::BlendFactor::OneMinusSrcAlpha, + alpha_blend_op: vk::BlendOp::Add, + color_write_mask: vk::ColorComponentFlags::RGBA, + }]; + + let color_blending = vk::PipelineColorBlendStateCreateInfo { + attachment_count: blend_attachment.len() as u32, + attachments: blend_attachment.as_ptr(), + ..Default::default() + }; + + let dynamic_states = [vk::DynamicState::Viewport, vk::DynamicState::Scissor]; + let dynamic_state = vk::PipelineDynamicStateCreateInfo { + dynamic_state_count: dynamic_states.len() as u32, + dynamic_states: dynamic_states.as_ptr(), + ..Default::default() + }; + + let pipeline_info = [vk::GraphicsPipelineCreateInfo { + stage_count: stages.len() as u32, + stages: stages.as_ptr(), + vertex_input_state: &vertex_input, + input_assembly_state: &input_assembly, + viewport_state: &viewport_state, + rasterization_state: &rasterizer, + multisample_state: &multisampling, + depth_stencil_state: &depth_stencil, + color_blend_state: &color_blending, + dynamic_state: &dynamic_state, + layout, + render_pass, + subpass: 0, + ..Default::default() + }]; + + let mut pipeline = vk::Pipeline::null(); + device + .create_graphics_pipelines( + vk::PipelineCache::null(), + &pipeline_info, + None, + slice::from_mut(&mut pipeline), + ) + .expect("failed to create gui rect pipeline"); + + device.destroy_shader_module(vert_module, None); + device.destroy_shader_module(frag_module, None); + + pipeline +} + +fn push_rect(verts: &mut Vec, rect: &RectCmd) { + let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); + let positions = [ + [x, y], + [x + w, y], + [x, y + h], + [x + w, y], + [x + w, y + h], + [x, y + h], + ]; + for pos in positions { + verts.push(Vertex { + pos, + color: rect.color, + mode: VertexMode::RECT, + rect_size: [w, h], + }); + } +} diff --git a/pomme-client/src/renderer/mod.rs b/pomme-client/src/renderer/mod.rs index 5483da39..fdc4140c 100644 --- a/pomme-client/src/renderer/mod.rs +++ b/pomme-client/src/renderer/mod.rs @@ -3,6 +3,7 @@ pub mod camera; pub mod chunk; mod context; pub mod entity_model; +mod gui_renderer; pub mod pipelines; mod screenshot; pub(crate) mod shader; @@ -39,6 +40,7 @@ pub use pipelines::particle::{ParticlePipeline, ParticleQuad}; use pipelines::skin_preview::SkinPreviewPipeline; pub use pipelines::sky::{SkyPipeline, SkyState}; pub use pipelines::weather::{WeatherColumn, WeatherPipeline}; +use pomme_gui::graphics::state::GuiRenderState; use pyronyx::khr::swapchain::{SwapchainDevice, SwapchainQueue}; use pyronyx::vk; use swapchain::Swapchain; @@ -49,6 +51,7 @@ use winit::window::Window; use crate::app::input::InputState; use crate::assets::AssetIndex; use crate::entity::components::{LookDirection, Position}; +use crate::renderer::gui_renderer::GuiRenderer; use crate::renderer::pipelines::chunk_borders::ChunkBorderPipeline; use crate::renderer::pipelines::item_entity::ItemEntityPipeline; use crate::world::block::registry::BlockRegistry; @@ -163,6 +166,8 @@ pub struct Renderer { gui_item_pipeline: pipelines::gui_item::GuiItemPipeline, gui_item_atlas: pipelines::gui_item_atlas::GuiItemAtlas, + gui_renderer: GuiRenderer, + atlas: TextureAtlas, entity_renderer: EntityRenderer, block_entity_pipeline: BlockEntityPipeline, @@ -208,6 +213,9 @@ impl Renderer { // requested size; track that actual extent so layout matches rendering. let swapchain_extent = swapchain_state.extent; + let gui_renderer = + GuiRenderer::new(&ctx.device, &ctx.allocator, swapchain_state.render_pass); + let mut menu_pipeline = MenuOverlayPipeline::new( &ctx.device, ctx.graphics_queue, @@ -477,6 +485,9 @@ impl Renderer { cloud_pipeline, gui_item_pipeline, gui_item_atlas, + + gui_renderer, + chunk_buffers, render_finished_per_image, screenshot: screenshot::ScreenshotCapture::new(game_dir.to_path_buf()), @@ -738,6 +749,8 @@ impl Renderer { self.blur_pipeline.blurred_view(), self.blur_pipeline.blurred_sampler(), ); + self.gui_renderer + .recreate_pipeline(&self.ctx.device, self.swapchain.render_pass); let sem_info = vk::SemaphoreCreateInfo::default(); self.render_finished_per_image = Vec::with_capacity(self.swapchain.images.len()); @@ -1047,6 +1060,7 @@ impl Renderer { player_preview: Option, book_preview: Option, eyes_in_water: bool, + gui_render_state: &mut GuiRenderState, ) -> Result<(), RendererError> { // Refresh the far plane before this frame's view/projection and fog. self.camera.set_render_distance(render_distance); @@ -1090,9 +1104,11 @@ impl Renderer { book_preview, eyes_in_water, }, + gui_render_state, ) } + #[allow(clippy::too_many_arguments)] pub fn render_menu( &mut self, window: &Window, @@ -1101,6 +1117,7 @@ impl Renderer { elements: Vec, cursor: (f32, f32), show_skin: bool, + gui_render_state: &mut GuiRenderState, ) -> Result<(), RendererError> { self.render_frame( window, @@ -1113,6 +1130,7 @@ impl Renderer { cursor, show_skin, }, + gui_render_state, ) } @@ -1309,6 +1327,7 @@ impl Renderer { hide_cursor: bool, clear_color: [f32; 4], mode: RenderMode<'_>, + gui_render_state: &mut GuiRenderState, ) -> Result<(), RendererError> { if self.swapchain_dirty { self.recreate_swapchain()?; @@ -1832,6 +1851,8 @@ impl Renderer { } } + self.gui_renderer.draw(cmd, sw, sh, gui_render_state); + cmd.end_render_pass(); // Image is now in PresentSrcKHR; grab it before present if F2 was pressed. @@ -2215,6 +2236,8 @@ impl Drop for Renderer { self.gui_item_atlas .destroy(&self.ctx.device, &self.ctx.allocator); self.atlas.destroy(&self.ctx.device, &self.ctx.allocator); + self.gui_renderer + .destroy(&self.ctx.device, &self.ctx.allocator); for sem in self.render_finished_per_image.drain(..) { self.ctx.device.destroy_semaphore(sem, None); diff --git a/pomme-client/src/renderer/shaders/gui_rect.frag b/pomme-client/src/renderer/shaders/gui_rect.frag new file mode 100644 index 00000000..747bc408 --- /dev/null +++ b/pomme-client/src/renderer/shaders/gui_rect.frag @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec4 in_color; +layout(location = 1) in vec2 in_local_pos; +layout(location = 2) in vec2 in_rect_size; + +layout(location = 0) out vec4 frag_color; + +void main() { + frag_color = in_color; +} diff --git a/pomme-client/src/renderer/shaders/gui_rect.vert b/pomme-client/src/renderer/shaders/gui_rect.vert new file mode 100644 index 00000000..36d7f84d --- /dev/null +++ b/pomme-client/src/renderer/shaders/gui_rect.vert @@ -0,0 +1,22 @@ +#version 450 + +layout(location = 0) in vec2 in_pos; +layout(location = 1) in vec4 in_color; +layout(location = 2) in float in_mode; +layout(location = 3) in vec2 in_rect_size; + +layout(location = 0) out vec4 out_color; +layout(location = 1) out vec2 out_local_pos; +layout(location = 2) out vec2 out_rect_size; + +layout(set = 0, binding = 0) uniform Globals { + vec2 screen_size; +}; + +void main() { + vec2 ndc = (in_pos / screen_size) * 2.0 - 1.0; + gl_Position = vec4(ndc, 0.0, 1.0); + out_color = in_color; + out_local_pos = in_pos; + out_rect_size = in_rect_size; +} diff --git a/pomme-gui/Cargo.toml b/pomme-gui/Cargo.toml new file mode 100644 index 00000000..9ff5113c --- /dev/null +++ b/pomme-gui/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pomme-gui" +version = "0.1.0" +edition = "2024" + +[dependencies] +winit.workspace = true +tracing.workspace = true diff --git a/pomme-gui/src/graphics/extractor.rs b/pomme-gui/src/graphics/extractor.rs new file mode 100644 index 00000000..c49d0260 --- /dev/null +++ b/pomme-gui/src/graphics/extractor.rs @@ -0,0 +1,61 @@ +use crate::graphics::state::{DrawCmd, GuiRenderState, Rect, RectCmd}; +use crate::types::{Position, Size}; + +pub struct GuiGraphicsExtractor<'a> { + render_state: &'a mut GuiRenderState, + blur_applied: bool, + gui_scale: i32, + + pub mouse_pos: Position, + pub mouse_x: i32, + pub mouse_y: i32, + pub screen_size: Size, + pub screen_width: i32, + pub screen_height: i32, +} + +impl<'a> GuiGraphicsExtractor<'a> { + pub fn new( + render_state: &'a mut GuiRenderState, + gui_scale: i32, + mouse_pos: Position, + screen_size: Size, + ) -> Self { + Self { + render_state, + blur_applied: false, + gui_scale, + + mouse_pos, + mouse_x: mouse_pos.x, + mouse_y: mouse_pos.y, + screen_size, + screen_width: screen_size.width, + screen_height: screen_size.height, + } + } + + /// Blur may only be applied once + pub const fn push_blur(&mut self) { + assert!(!self.blur_applied); + self.blur_applied = true; + } + + pub fn push_cmd(&mut self, cmd: DrawCmd) { + if !self.blur_applied { + self.render_state.before_blur_cmds.push(cmd); + } else { + self.render_state.after_blur_cmds.push(cmd); + } + } + + pub fn push_rect(&mut self, rect: Rect) { + self.push_cmd(DrawCmd::Rect(RectCmd { + x: (rect.x * self.gui_scale) as f32, + y: (rect.y * self.gui_scale) as f32, + width: (rect.width * self.gui_scale) as f32, + height: (rect.height * self.gui_scale) as f32, + color: rect.color.into(), + })); + } +} diff --git a/pomme-gui/src/graphics/mod.rs b/pomme-gui/src/graphics/mod.rs new file mode 100644 index 00000000..5a9a3067 --- /dev/null +++ b/pomme-gui/src/graphics/mod.rs @@ -0,0 +1,2 @@ +pub mod extractor; +pub mod state; diff --git a/pomme-gui/src/graphics/state.rs b/pomme-gui/src/graphics/state.rs new file mode 100644 index 00000000..1a168498 --- /dev/null +++ b/pomme-gui/src/graphics/state.rs @@ -0,0 +1,35 @@ +use crate::types::Color; + +#[derive(Default)] +pub struct GuiRenderState { + pub before_blur_cmds: Vec, + pub after_blur_cmds: Vec, +} + +impl GuiRenderState { + pub fn clear(&mut self) { + self.before_blur_cmds.clear(); + self.after_blur_cmds.clear(); + } +} + +pub enum DrawCmd { + Rect(RectCmd), +} + +pub struct RectCmd { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub color: [f32; 4], +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub color: Color, +} diff --git a/pomme-gui/src/lib.rs b/pomme-gui/src/lib.rs new file mode 100644 index 00000000..3a018a03 --- /dev/null +++ b/pomme-gui/src/lib.rs @@ -0,0 +1,128 @@ +use crate::graphics::extractor::GuiGraphicsExtractor; +use crate::graphics::state::GuiRenderState; +use crate::screen::Screen; +use crate::screens::title_screen::TitleScreen; +use crate::types::{Position, Size}; + +pub mod graphics; +pub mod screen; +pub mod screens; +pub mod types; + +pub struct Gui { + screen_size: Size, + gui_scale_setting: u32, + enforce_unicode: bool, + gui_scale: i32, + scaled_screen_size: Size, + + gui_render_state: GuiRenderState, + + screen: Option>, +} + +impl Gui { + const MIN_WIDTH: i32 = 320; + const MIN_HEIGHT: i32 = 240; + + pub fn new( + screen_size: impl Into, + gui_scale_setting: u32, + enforce_unicode: bool, + ) -> Self { + let screen_size = screen_size.into(); + let gui_scale = Self::calculate_gui_scale(screen_size, gui_scale_setting, enforce_unicode); + let scaled_screen_size = Self::calculate_scaled_screen_size(gui_scale, screen_size); + + Self { + screen_size, + gui_scale_setting, + enforce_unicode, + gui_scale, + scaled_screen_size, + + gui_render_state: GuiRenderState::default(), + + screen: Some(Box::new(TitleScreen::default())), + } + } + + pub fn resize(&mut self, screen_size: impl Into) { + self.screen_size = screen_size.into(); + self.recalculate(); + } + + /// TODO: Call when the user changes the gui scale setting (0 = auto). + fn set_gui_scale_setting(&mut self, gui_scale_setting: u32) { + self.gui_scale_setting = gui_scale_setting; + self.recalculate(); + } + + /// TODO: Call when Unicode font enforcement is toggled. + fn set_enforce_unicode(&mut self, enforce_unicode: bool) { + self.enforce_unicode = enforce_unicode; + self.recalculate(); + } + + fn set_screen(&mut self, screen: Option) { + self.screen = screen.map(|s| Box::new(s) as Box); + self.recalculate(); + } + + pub fn extract_render_state(&mut self, mouse_pos: impl Into) -> &mut GuiRenderState { + self.gui_render_state.clear(); + if let Some(screen) = self.screen.as_deref_mut() { + let mut graphics = GuiGraphicsExtractor::new( + &mut self.gui_render_state, + self.gui_scale, + mouse_pos.into(), + self.scaled_screen_size, + ); + screen.extract_render_state(&mut graphics, 1.0); + } + &mut self.gui_render_state + } + + /// Recomputes `gui_scale` and `scaled_screen_size` from current state. + fn recalculate(&mut self) { + self.gui_scale = Self::calculate_gui_scale( + self.screen_size, + self.gui_scale_setting, + self.enforce_unicode, + ); + self.scaled_screen_size = + Self::calculate_scaled_screen_size(self.gui_scale, self.screen_size); + } + + fn calculate_max_gui_scale(screen_size: Size) -> i32 { + (screen_size.width / Self::MIN_WIDTH) + .min(screen_size.height / Self::MIN_HEIGHT) + .max(1) + } + + fn calculate_gui_scale( + screen_size: Size, + gui_scale_setting: u32, + enforce_unicode: bool, + ) -> i32 { + let max = Self::calculate_max_gui_scale(screen_size); + let mut scale = if gui_scale_setting == 0 { + max + } else { + (gui_scale_setting as i32).min(max) + }; + + if enforce_unicode && scale % 2 != 0 { + scale += 1; + } + + scale.min(max) + } + + fn calculate_scaled_screen_size(gui_scale: i32, screen_size: Size) -> Size { + let scale = gui_scale as f64; + let width = (screen_size.width as f64 / scale).ceil() as i32; + let height = (screen_size.height as f64 / scale).ceil() as i32; + Size::new(width, height) + } +} diff --git a/pomme-gui/src/screen.rs b/pomme-gui/src/screen.rs new file mode 100644 index 00000000..ace13e5b --- /dev/null +++ b/pomme-gui/src/screen.rs @@ -0,0 +1,5 @@ +use crate::graphics::extractor::GuiGraphicsExtractor; + +pub trait Screen { + fn extract_render_state(&mut self, graphics: &mut GuiGraphicsExtractor, alpha: f32); +} diff --git a/pomme-gui/src/screens/mod.rs b/pomme-gui/src/screens/mod.rs new file mode 100644 index 00000000..81f137d4 --- /dev/null +++ b/pomme-gui/src/screens/mod.rs @@ -0,0 +1 @@ +pub mod title_screen; diff --git a/pomme-gui/src/screens/title_screen.rs b/pomme-gui/src/screens/title_screen.rs new file mode 100644 index 00000000..57b713cc --- /dev/null +++ b/pomme-gui/src/screens/title_screen.rs @@ -0,0 +1,19 @@ +use crate::graphics::extractor::GuiGraphicsExtractor; +use crate::graphics::state::Rect; +use crate::screen::Screen; +use crate::types::Color; + +#[derive(Default)] +pub struct TitleScreen {} + +impl Screen for TitleScreen { + fn extract_render_state(&mut self, graphics: &mut GuiGraphicsExtractor, alpha: f32) { + graphics.push_rect(Rect { + x: 0, + y: 0, + width: graphics.screen_width, + height: graphics.screen_height, + color: Color::WHITE, + }); + } +} diff --git a/pomme-gui/src/types.rs b/pomme-gui/src/types.rs new file mode 100644 index 00000000..80bbfb6b --- /dev/null +++ b/pomme-gui/src/types.rs @@ -0,0 +1,71 @@ +use winit::dpi::{PhysicalPosition, PhysicalSize, Pixel}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +impl Position { + pub const fn new(x: i32, y: i32) -> Self { + Self { x, y } + } +} + +impl From> for Position { + fn from(value: PhysicalPosition

) -> Self { + Self { + x: value.x.cast(), + y: value.y.cast(), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct Size { + pub width: i32, + pub height: i32, +} + +impl Size { + pub const fn new(width: i32, height: i32) -> Self { + Self { width, height } + } +} + +impl From> for Size { + fn from(value: PhysicalSize

) -> Self { + Self { + width: value.width.cast(), + height: value.height.cast(), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct Color { + r: u8, + g: u8, + b: u8, + a: f32, +} + +impl Color { + pub const WHITE: Self = Self::new(255, 255, 255, 1.0); + pub const BLACK: Self = Self::new(0, 0, 0, 1.0); + + pub const fn new(r: u8, g: u8, b: u8, a: f32) -> Self { + Self { r, g, b, a } + } +} + +impl From for [f32; 4] { + fn from(value: Color) -> Self { + [ + value.r as f32 / 255.0, + value.g as f32 / 255.0, + value.b as f32 / 255.0, + value.a, + ] + } +}