ttf.rsannotatedttf.rssource302 lines · 10.0 KB · raw
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}