1use bytemuck::{Pod, Zeroable}; 2use glyphon::{ 3 Attrs, Buffer, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea, 4 TextAtlas, TextBounds, TextRenderer, 5}; 6use jgenesis_common::timeutils; 7use std::borrow::Cow; 8use std::time::Duration; 9use wgpu::util::DeviceExt; 10 11const FONT_SIZE: f32 = 30.0; 12const LINE_HEIGHT: f32 = 60.0; 13const BORDER_OFFSET: f32 = 20.0; 14const BOX_OFFSET: f32 = 7.5; 15 16#[repr(C)] 17#[derive(Debug, Clone, Copy, Zeroable, Pod)] 18struct Vertex { 19 position: [f32; 2], 20} 21 22impl Vertex { 23 const ATTRIBUTES: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2]; 24 25 const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { 26 array_stride: size_of::<Self>() as u64, 27 step_mode: wgpu::VertexStepMode::Vertex, 28 attributes: &Self::ATTRIBUTES, 29 }; 30} 31 32pub struct Modal { 33 id: Option<Cow<'static, str>>, 34 text: String, 35 insertion_nanos: u128, 36 expiry_nanos: u128, 37} 38 39#[derive(Debug, Clone, Copy, PartialEq, Eq)] 40pub enum Multisampled { 41 No, 42 Yes, 43} 44 45const MULTISAMPLE_STATE: wgpu::MultisampleState = 46 wgpu::MultisampleState { count: 4, mask: !0, alpha_to_coverage_enabled: false }; 47 48pub struct ModalRenderer { 49 font_system: FontSystem, 50 swash_cache: SwashCache, 51 viewport: glyphon::Viewport, 52 atlas: TextAtlas, 53 text_renderer: TextRenderer, 54 text_renderer_multisample: TextRenderer, 55 buffers: Vec<Buffer>, 56 modals: Vec<Modal>, 57 bg_pipeline: wgpu::RenderPipeline, 58 bg_pipeline_multisample: wgpu::RenderPipeline, 59} 60 61impl ModalRenderer { 62 pub fn new( 63 device: &wgpu::Device, 64 queue: &wgpu::Queue, 65 surface_format: wgpu::TextureFormat, 66 ) -> Self { 67 let font_system = FontSystem::new(); 68 let swash_cache = SwashCache::new(); 69 let glyphon_cache = glyphon::Cache::new(device); 70 let mut atlas = TextAtlas::new(device, queue, &glyphon_cache, surface_format); 71 let text_renderer = 72 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None); 73 let text_renderer_multisample = 74 TextRenderer::new(&mut atlas, device, MULTISAMPLE_STATE, None); 75 76 let shader = device.create_shader_module(wgpu::include_wgsl!("wgsl/modal.wgsl")); 77 let bg_pipeline_descriptor = wgpu::RenderPipelineDescriptor { 78 label: "modal_bg_pipeline".into(), 79 layout: None, 80 vertex: wgpu::VertexState { 81 module: &shader, 82 entry_point: None, 83 compilation_options: wgpu::PipelineCompilationOptions::default(), 84 buffers: &[Vertex::LAYOUT], 85 }, 86 primitive: wgpu::PrimitiveState { 87 topology: wgpu::PrimitiveTopology::TriangleList, 88 strip_index_format: None, 89 front_face: wgpu::FrontFace::Ccw, 90 cull_mode: None, 91 unclipped_depth: false, 92 polygon_mode: wgpu::PolygonMode::Fill, 93 conservative: false, 94 }, 95 depth_stencil: None, 96 multisample: wgpu::MultisampleState::default(), 97 fragment: Some(wgpu::FragmentState { 98 module: &shader, 99 entry_point: None, 100 compilation_options: wgpu::PipelineCompilationOptions::default(), 101 targets: &[Some(wgpu::ColorTargetState { 102 format: surface_format, 103 blend: Some(wgpu::BlendState::ALPHA_BLENDING), 104 write_mask: wgpu::ColorWrites::ALL, 105 })], 106 }), 107 multiview_mask: None, 108 cache: None, 109 }; 110 let bg_pipeline = device.create_render_pipeline(&bg_pipeline_descriptor); 111 let bg_pipeline_multisample = 112 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 113 multisample: MULTISAMPLE_STATE, 114 ..bg_pipeline_descriptor 115 }); 116 117 let viewport = glyphon::Viewport::new(device, &glyphon_cache); 118 119 Self { 120 font_system, 121 swash_cache, 122 viewport, 123 atlas, 124 text_renderer, 125 text_renderer_multisample, 126 buffers: Vec::with_capacity(10), 127 modals: Vec::with_capacity(10), 128 bg_pipeline, 129 bg_pipeline_multisample, 130 } 131 } 132 133 pub fn add_or_update_modal( 134 &mut self, 135 id: Option<Cow<'static, str>>, 136 text: String, 137 duration: Duration, 138 ) { 139 let current_time_nanos = timeutils::current_time_nanos(); 140 let expiry_nanos = current_time_nanos + duration.as_nanos(); 141 142 if let Some(id) = &id { 143 for modal in &mut self.modals { 144 if modal.id.as_ref().is_some_and(|other_id| other_id == id) { 145 modal.text = text; 146 modal.insertion_nanos = current_time_nanos; 147 modal.expiry_nanos = expiry_nanos; 148 149 self.modals.sort_by_key(|modal| modal.insertion_nanos); 150 151 return; 152 } 153 } 154 } 155 156 self.modals.push(Modal { id, text, insertion_nanos: current_time_nanos, expiry_nanos }); 157 } 158 159 pub fn prepare_modals( 160 &mut self, 161 device: &wgpu::Device, 162 queue: &wgpu::Queue, 163 multisampled: Multisampled, 164 width: u32, 165 height: u32, 166 ) -> Result<Option<wgpu::Buffer>, glyphon::PrepareError> { 167 let now_nanos = timeutils::current_time_nanos(); 168 self.modals.retain(|modal| modal.expiry_nanos > now_nanos); 169 170 if self.modals.is_empty() { 171 return Ok(None); 172 } 173 174 while self.buffers.len() < self.modals.len() { 175 self.buffers 176 .push(Buffer::new(&mut self.font_system, Metrics::new(FONT_SIZE, LINE_HEIGHT))); 177 } 178 179 let font_attrs = Attrs::new().family(Family::Monospace); 180 181 let mut vertices = Vec::with_capacity(self.modals.len()); 182 let mut text_areas = Vec::with_capacity(self.modals.len()); 183 let mut line_top = BORDER_OFFSET; 184 for (modal, buffer) in self.modals.iter().zip(self.buffers.iter_mut()) { 185 buffer.set_size(&mut self.font_system, Some(width as f32), Some(height as f32)); 186 buffer.set_text(&mut self.font_system, &modal.text, &font_attrs, Shaping::Basic, None); 187 buffer.shape_until_scroll(&mut self.font_system, false); 188 189 text_areas.push(TextArea { 190 buffer, 191 left: BORDER_OFFSET, 192 top: line_top, 193 scale: 1.0, 194 bounds: TextBounds { left: 0, top: 0, right: width as i32, bottom: height as i32 }, 195 default_color: glyphon::Color::rgb(255, 255, 255), 196 custom_glyphs: &[], 197 }); 198 199 let box_vertices = 200 determine_box_positions(buffer, line_top, width as f32, height as f32); 201 vertices.extend([ 202 box_vertices[0], 203 box_vertices[1], 204 box_vertices[2], 205 box_vertices[1], 206 box_vertices[2], 207 box_vertices[3], 208 ]); 209 210 line_top += LINE_HEIGHT + BORDER_OFFSET; 211 } 212 213 self.viewport.update(queue, Resolution { width, height }); 214 215 let text_renderer = match multisampled { 216 Multisampled::No => &mut self.text_renderer, 217 Multisampled::Yes => &mut self.text_renderer_multisample, 218 }; 219 220 text_renderer.prepare( 221 device, 222 queue, 223 &mut self.font_system, 224 &mut self.atlas, 225 &self.viewport, 226 text_areas, 227 &mut self.swash_cache, 228 )?; 229 230 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 231 label: "modal_bg_vertex_buffer".into(), 232 contents: bytemuck::cast_slice(&vertices), 233 usage: wgpu::BufferUsages::VERTEX, 234 }); 235 236 Ok(Some(vertex_buffer)) 237 } 238 239 pub fn render<'rpass>( 240 &'rpass self, 241 multisampled: Multisampled, 242 vertex_buffer: &'rpass wgpu::Buffer, 243 render_pass: &mut wgpu::RenderPass<'rpass>, 244 ) -> Result<(), glyphon::RenderError> { 245 if self.modals.is_empty() { 246 return Ok(()); 247 } 248 249 render_pass.set_pipeline(match multisampled { 250 Multisampled::No => &self.bg_pipeline, 251 Multisampled::Yes => &self.bg_pipeline_multisample, 252 }); 253 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..)); 254 255 let vertex_count = 6 * self.modals.len() as u32; 256 render_pass.draw(0..vertex_count, 0..1); 257 258 let text_renderer = match multisampled { 259 Multisampled::No => &self.text_renderer, 260 Multisampled::Yes => &self.text_renderer_multisample, 261 }; 262 text_renderer.render(&self.atlas, &self.viewport, render_pass) 263 } 264} 265 266fn determine_box_positions(buffer: &Buffer, line_top: f32, width: f32, height: f32) -> [Vertex; 4] { 267 let text_line = &buffer.lines[0].layout_opt().as_ref().unwrap()[0]; 268 let text_width = text_line.w; 269 let max_ascent = text_line.max_ascent; 270 let max_descent = text_line.max_descent; 271 272 let line_left = BORDER_OFFSET; 273 274 let center_offset = (LINE_HEIGHT - max_ascent - max_descent) / 2.0; 275 let line_v_center = line_top + max_ascent + center_offset; 276 277 let unnormalized = [ 278 Vertex { position: [line_left - BOX_OFFSET, line_v_center - max_ascent - BOX_OFFSET] }, 279 Vertex { position: [line_left - BOX_OFFSET, line_v_center + max_descent + BOX_OFFSET] }, 280 Vertex { 281 position: [ 282 line_left + text_width + BOX_OFFSET, 283 line_v_center - max_ascent - BOX_OFFSET, 284 ], 285 }, 286 Vertex { 287 position: [ 288 line_left + text_width + BOX_OFFSET, 289 line_v_center + max_descent + BOX_OFFSET, 290 ], 291 }, 292 ]; 293 294 let half_width = 0.5 * width; 295 let half_height = 0.5 * height; 296 unnormalized.map(|v| Vertex { 297 position: [ 298 (v.position[0] - half_width) / half_width, 299 -(v.position[1] - half_height) / half_height, 300 ], 301 }) 302}