/* wearechat - generic multiplayer game with voip Copyright (C) 2025 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use super::GraphicsConfig; use crate::state::InputState; use egui::{ Context, Event, ImageData, PointerButton, TextureId, ViewportId, ViewportInfo, epaint::{ImageDelta, Primitive, Vertex}, }; use glam::{Affine3A, Mat2, Mat3, Mat4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles, vec2, vec4}; use log::{debug, warn}; use rand::random; use std::{ collections::HashMap, num::NonZeroU64, sync::{Arc, RwLock}, }; use wgpu::{ AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, Buffer, BufferDescriptor, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CompareFunction, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, FrontFace, ImageCopyTexture, ImageDataLayout, IndexFormat, LoadOp, MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, Queue, RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderStages, StoreOp, SurfaceConfiguration, Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, VertexBufferLayout, VertexState, VertexStepMode, include_wgsl, util::{DeviceExt, TextureDataOrder}, vertex_attr_array, }; use winit::event::MouseButton; pub const UI_POSITION_OFFSET: f32 = 1000.; pub struct UiRenderer { device: Arc, queue: Arc, config: GraphicsConfig, ctx: Context, pipeline: RenderPipeline, format: TextureFormat, pipeline_layout: PipelineLayout, bind_group_layout: BindGroupLayout, textures: RwLock>, surfaces: RwLock>, last_pointer: Vec2, } pub struct UiSurface { pub transform: Affine3A, pub content: Arc bool + Send + Sync + 'static>, size: Vec2, index: Buffer, index_capacity: usize, vertex: Buffer, vertex_capacity: usize, } pub enum UiEvent { Click(Vec2, MouseButton, bool), } impl UiRenderer { pub fn new( device: Arc, queue: Arc, format: TextureFormat, config: GraphicsConfig, ) -> Self { let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ BindGroupLayoutEntry { binding: 0, count: None, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { sample_type: TextureSampleType::Float { filterable: true }, view_dimension: TextureViewDimension::D2, multisampled: false, }, }, BindGroupLayoutEntry { binding: 1, count: None, visibility: ShaderStages::FRAGMENT, ty: BindingType::Sampler(SamplerBindingType::Filtering), }, ], label: None, }); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[PushConstantRange { range: 0..(4 * 4 * size_of::() as u32), stages: ShaderStages::VERTEX, }], }); let pipeline = Self::create_pipeline(&device, &pipeline_layout, format, config.sample_count); Self { ctx: Context::default(), pipeline, device, queue, pipeline_layout, bind_group_layout, config, format, last_pointer: Vec2::ZERO, textures: HashMap::new().into(), surfaces: HashMap::new().into(), } } fn create_pipeline( device: &Device, pipeline_layout: &PipelineLayout, format: TextureFormat, sample_count: u32, ) -> RenderPipeline { let frag_shader = device.create_shader_module(include_wgsl!("shaders/fragment_ui.wgsl")); let vert_shader = device.create_shader_module(include_wgsl!("shaders/vertex_ui.wgsl")); device.create_render_pipeline(&RenderPipelineDescriptor { label: Some("ui pipeline"), layout: Some(&pipeline_layout), fragment: Some(FragmentState { module: &frag_shader, entry_point: Some("main"), targets: &[Some(ColorTargetState { blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), format, write_mask: ColorWrites::all(), })], compilation_options: PipelineCompilationOptions::default(), }), vertex: VertexState { module: &vert_shader, entry_point: Some("main"), buffers: &[VertexBufferLayout { array_stride: size_of::() as u64, step_mode: VertexStepMode::Vertex, attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], }], compilation_options: PipelineCompilationOptions::default(), }, primitive: PrimitiveState { topology: PrimitiveTopology::TriangleList, front_face: FrontFace::Ccw, cull_mode: None, //Some(Face::Back), polygon_mode: PolygonMode::Fill, ..Default::default() }, depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth32Float, depth_write_enabled: false, depth_compare: CompareFunction::LessEqual, stencil: Default::default(), bias: Default::default(), }), multisample: MultisampleState { count: sample_count, ..Default::default() }, multiview: None, cache: None, }) } pub fn reconfigure(&mut self, config: &GraphicsConfig) { if self.config.sample_count != config.sample_count { self.pipeline = Self::create_pipeline( &self.device, &self.pipeline_layout, self.format, config.sample_count, ); self.config.sample_count = config.sample_count; } } pub fn add_surface( &mut self, transform: Affine3A, content: impl Fn(&Context) -> bool + Send + Sync + 'static, ) { let index_capacity = 1024; let vertex_capacity = 1024; let index = self.device.create_buffer(&BufferDescriptor { label: None, size: (size_of::() * index_capacity) as u64, usage: BufferUsages::INDEX | BufferUsages::COPY_DST, mapped_at_creation: false, }); let vertex = self.device.create_buffer(&BufferDescriptor { label: None, size: (size_of::() * vertex_capacity) as u64, usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, mapped_at_creation: false, }); let id = ViewportId::from_hash_of(random::()); debug!("ui surface added: {id:?}"); self.surfaces.write().unwrap().insert(id, UiSurface { transform, content: Arc::new(content), index, vertex, index_capacity, vertex_capacity, size: Vec2::ZERO, }); } pub fn apply_texture_delta(&self, texid: TextureId, delta: ImageDelta) { let mut textures = self.textures.write().unwrap(); let size = Extent3d { depth_or_array_layers: 1, width: delta.image.width() as u32, height: delta.image.height() as u32, }; let pixels = match &delta.image { ImageData::Color(color_image) => color_image.pixels.clone(), ImageData::Font(font_image) => font_image.srgba_pixels(None).collect(), }; if let Some((_texbg, tex, texsize)) = textures.get_mut(&texid) { let pos = delta.pos.unwrap_or([0, 0]); debug!("updating UI texture at {pos:?}"); self.queue.write_texture( ImageCopyTexture { texture: &tex, mip_level: 0, origin: Origin3d { x: pos[0] as u32, y: pos[1] as u32, z: 0, }, aspect: TextureAspect::All, }, bytemuck::cast_slice::<_, u8>(&pixels), ImageDataLayout { offset: 0, bytes_per_row: Some(texsize[0] * 4), rows_per_image: None, }, size, ); } else { assert_eq!( delta.pos, None, "partial update impossible; texture does not yet exist" ); debug!( "uploading new UI texture: width={}, height={}", delta.image.width(), delta.image.height() ); let texture = self.device.create_texture_with_data( &self.queue, &TextureDescriptor { label: None, size, mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, format: TextureFormat::Rgba8UnormSrgb, usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }, TextureDataOrder::LayerMajor, bytemuck::cast_slice::<_, u8>(&pixels), ); let textureview = texture.create_view(&TextureViewDescriptor::default()); let sampler = self.device.create_sampler(&SamplerDescriptor { address_mode_u: AddressMode::ClampToEdge, address_mode_v: AddressMode::ClampToEdge, mag_filter: FilterMode::Linear, min_filter: FilterMode::Linear, ..Default::default() }); let bindgroup = self.device.create_bind_group(&BindGroupDescriptor { label: None, layout: &self.bind_group_layout, entries: &[ BindGroupEntry { binding: 0, resource: BindingResource::TextureView(&textureview), }, BindGroupEntry { binding: 1, resource: BindingResource::Sampler(&sampler), }, ], }); textures.insert( texid, (bindgroup, texture, delta.image.size().map(|e| e as u32)), ); } } pub fn draw( &mut self, commands: &mut CommandEncoder, target: &TextureView, resolve_target: Option<&TextureView>, depth: &TextureView, projection: Mat4, input_state: &mut InputState, surface_configuration: &SurfaceConfiguration, ) { let mut surfaces = self.surfaces.write().unwrap(); if surfaces.is_empty() { return; } let mut rpass = commands.begin_render_pass(&RenderPassDescriptor { label: None, color_attachments: &[Some(RenderPassColorAttachment { view: target, resolve_target, ops: Operations { load: LoadOp::Load, store: StoreOp::Store, }, })], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &depth, depth_ops: Some(Operations { load: LoadOp::Load, store: StoreOp::Store, }), stencil_ops: None, }), ..Default::default() }); rpass.set_pipeline(&self.pipeline); let mut raw_input = egui::RawInput::default(); raw_input.viewport_id = surfaces.keys().next().copied().unwrap(); raw_input.viewports = surfaces .keys() .map(|k| { (*k, ViewportInfo { native_pixels_per_point: Some(2.), ..Default::default() }) }) .collect(); let mut surfaces_closed = Vec::new(); for (viewport_id, surf) in surfaces.iter_mut() { let scale = 0.005; let projection = projection * Mat4::from_translation(surf.transform.translation.into()) * Mat4::from_mat3a(surf.transform.matrix3) * Mat4::from_mat3(Mat3::from_mat2(Mat2::from_cols_array(&[ scale, 0., 0., -scale, ]))) * Mat4::from_translation(-Vec3::new(UI_POSITION_OFFSET, UI_POSITION_OFFSET, 0.)); let screen_size = vec2( surface_configuration.width as f32, surface_configuration.height as f32, ); if projection.determinant() < 0e-4 { warn!("bad UI projection") } let unproject = projection.inverse(); let unproject_mouse = |pos: Vec2| { let mouse_xy_clip = (pos / screen_size) * 2. - 1.; let mouse_clip_1 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 0.0, 1.0); let mouse_clip_2 = vec4(mouse_xy_clip.x, -mouse_xy_clip.y, 1.0, 1.0); let mut mouse_world_1 = unproject * mouse_clip_1; let mut mouse_world_2 = unproject * mouse_clip_2; mouse_world_1 /= mouse_world_1.w; mouse_world_2 /= mouse_world_2.w; let mouse_world_1 = mouse_world_1.xyz(); let mouse_world_2 = mouse_world_2.xyz(); let ray_norm = (mouse_world_2 - mouse_world_1).normalize(); let ray_t = mouse_world_1.z / ray_norm.z; let ray_hit = mouse_world_1 - ray_norm * ray_t; debug_assert!(ray_hit.z.abs() < 0.1, "mouse was not projected properly"); ray_hit.xy() }; let cursor_pos = unproject_mouse(input_state.cursor_pos); let mut raw_input = raw_input.clone(); if cursor_pos != self.last_pointer { raw_input.events.push(Event::PointerMoved(egui::Pos2::new( cursor_pos.x, cursor_pos.y, ))); self.last_pointer = cursor_pos; } raw_input .events .extend(input_state.ui_events.iter().map(|e| match e { UiEvent::Click(pos, button, down) => egui::Event::PointerButton { pos: egui::Pos2::from(unproject_mouse(*pos).to_array()), button: match button { MouseButton::Left => PointerButton::Primary, MouseButton::Right => PointerButton::Secondary, MouseButton::Middle => PointerButton::Middle, MouseButton::Back => PointerButton::Extra1, MouseButton::Forward => PointerButton::Extra2, MouseButton::Other(_) => PointerButton::Extra1, }, pressed: *down, modifiers: egui::Modifiers::default(), }, })); let mut close = false; let full_output = self.ctx.run(raw_input.clone(), |ctx| { close = !(surf.content)(ctx); surf.size = Vec2::new(ctx.used_size().x, ctx.used_size().y) }); if close { surfaces_closed.push(*viewport_id) } for (texid, delta) in full_output.textures_delta.set { self.apply_texture_delta(texid, delta); } let clipped_primitives = self .ctx .tessellate(full_output.shapes, full_output.pixels_per_point); let mut index_count = 0; let mut vertex_count = 0; for p in &clipped_primitives { if let Primitive::Mesh(mesh) = &p.primitive { index_count += mesh.indices.len(); vertex_count += mesh.vertices.len(); } } if index_count == 0 || vertex_count == 0 { return; } while index_count > surf.index_capacity { debug!( "index buffer overflow ({index_count}). expanding {} -> {}", surf.index_capacity, surf.index_capacity * 2 ); surf.index_capacity *= 2; surf.index = self.device.create_buffer(&BufferDescriptor { label: None, size: (size_of::() * surf.index_capacity) as u64, usage: BufferUsages::INDEX | BufferUsages::COPY_DST, mapped_at_creation: false, }); } while vertex_count > surf.vertex_capacity { debug!( "vertex buffer overflow ({vertex_count}). expanding {} -> {}", surf.vertex_capacity, surf.vertex_capacity * 2 ); surf.vertex_capacity *= 2; surf.vertex = self.device.create_buffer(&BufferDescriptor { label: None, size: (size_of::() * surf.vertex_capacity) as u64, usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, mapped_at_creation: false, }); } let mut mapped_index = self .queue .write_buffer_with( &surf.index, 0, NonZeroU64::new((size_of::() * index_count) as u64).unwrap(), ) .expect("ui index buffer overflow"); let mut mapped_vertex = self .queue .write_buffer_with( &surf.vertex, 0, NonZeroU64::new((size_of::() * vertex_count) as u64).unwrap(), ) .expect("ui vertex buffer overflow"); let mut index_offset = 0; let mut vertex_offset = 0; let mut slices = Vec::new(); for p in clipped_primitives { if let Primitive::Mesh(mesh) = p.primitive { mapped_index[index_offset * size_of::() ..(index_offset + mesh.indices.len()) * size_of::()] .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); mapped_vertex[vertex_offset * size_of::() ..(vertex_offset + mesh.vertices.len()) * size_of::()] .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); slices.push(( index_offset as u32..index_offset as u32 + mesh.indices.len() as u32, vertex_offset as i32, mesh.texture_id, )); index_offset += mesh.indices.len(); vertex_offset += mesh.vertices.len(); } } assert_eq!(index_count, index_offset); assert_eq!(vertex_count, vertex_offset); let projection = projection.to_cols_array().map(|v| v.to_le_bytes()); rpass.set_push_constants(ShaderStages::VERTEX, 0, projection.as_flattened()); rpass.set_index_buffer(surf.index.slice(..), IndexFormat::Uint32); rpass.set_vertex_buffer(0, surf.vertex.slice(..)); for (index, base_vertex, texid) in slices { let tex_guard = self.textures.read().unwrap(); let bind_group = &tex_guard.get(&texid).unwrap().0; rpass.set_bind_group(0, bind_group, &[]); rpass.draw_indexed(index, base_vertex, 0..1); } } for s in surfaces_closed { debug!("ui surface closed: {s:?}"); surfaces.remove(&s); } input_state.ui_events.clear(); } }