1use crate::renderer::custom::pixel_font;
16use crate::renderer::util::TextureAtlas;
17use khora_core::asset::{font::Font, AssetUUID, Handle};
18use khora_core::math::{LinearRgba, Vec2, Vec4};
19use khora_core::renderer::{
20 api::command::{
21 BindGroupDescriptor, BindGroupEntry, BindGroupId, BindGroupLayoutDescriptor,
22 BindGroupLayoutEntry, BindGroupLayoutId, BindingResource, BindingType, BufferBinding,
23 LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
24 },
25 api::core::{ShaderModuleDescriptor, ShaderSourceData},
26 api::pipeline::{
27 ColorTargetStateDescriptor, ColorWrites, MultisampleStateDescriptor,
28 PipelineLayoutDescriptor, PrimitiveStateDescriptor, PrimitiveTopology,
29 RenderPipelineDescriptor, RenderPipelineId,
30 },
31 api::resource::{
32 AddressMode, BufferDescriptor, BufferId, BufferUsage, FilterMode, MipmapFilterMode,
33 SamplerDescriptor, SamplerId, TextureViewId,
34 },
35 api::text::{TextLayout, TextRenderer},
36 api::util::{IndexFormat, SampleCount, ShaderStageFlags, TextureFormat},
37 traits::CommandEncoder,
38 GraphicsDevice,
39};
40use std::any::Any;
41use std::borrow::Cow;
42use std::collections::HashMap;
43use std::sync::{Arc, Mutex};
44
45#[repr(C)]
47#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
48pub struct TextVertex {
49 pub pos: [f32; 2],
51 pub uv: [f32; 2],
53 pub color: [f32; 4],
55}
56
57pub struct StandardTextLayout {
59 pub size: Vec2,
61 pub glyph_positions: Vec<(u32, Vec2)>,
63 pub font_handle: Handle<Font>,
65 pub font_uuid: AssetUUID,
67 pub font_size: f32,
69}
70
71impl TextLayout for StandardTextLayout {
72 fn size(&self) -> Vec2 {
73 self.size
74 }
75
76 fn as_any(&self) -> &dyn Any {
77 self
78 }
79}
80
81struct TextGpuResources {
83 pipeline: RenderPipelineId,
84 #[allow(dead_code)]
85 bind_group_layout: BindGroupLayoutId,
86 #[allow(dead_code)]
87 sampler: SamplerId,
88 atlas: TextureAtlas,
89 bind_group: BindGroupId,
90 vertex_buffer: BufferId,
91 index_buffer: BufferId,
92}
93
94pub struct StandardTextRenderer {
96 glyph_cache: Mutex<HashMap<(AssetUUID, char, u32), GlyphData>>, queue: Mutex<Vec<QueuedText>>,
98 gpu_resources: Mutex<Option<TextGpuResources>>,
99 shader_source: String,
100}
101
102struct QueuedText {
103 layout: Arc<StandardTextLayout>,
104 pos: Vec2,
105 color: Vec4,
106}
107
108#[derive(Clone, Copy)]
109struct GlyphData {
110 pub uv_min: Vec2,
111 pub uv_max: Vec2,
112 #[allow(dead_code)]
113 pub size: Vec2,
114}
115
116impl StandardTextRenderer {
117 pub fn new(shader_source: String) -> Self {
119 Self {
120 glyph_cache: Mutex::new(HashMap::new()),
121 queue: Mutex::new(Vec::new()),
122 gpu_resources: Mutex::new(None),
123 shader_source,
124 }
125 }
126
127 fn rasterize_glyph(&self, c: char) -> Option<(u32, u32, Vec<u8>)> {
128 pixel_font::rasterize_glyph(c)
129 }
130
131 fn init_resources(
132 &self,
133 device: &dyn GraphicsDevice,
134 ) -> Result<TextGpuResources, Box<dyn std::error::Error + Send + Sync>> {
135 let atlas_size = 1024;
136
137 let shader_module = device.create_shader_module(&ShaderModuleDescriptor {
139 label: Some("text_shader"),
140 source: ShaderSourceData::Wgsl(Cow::Borrowed(&self.shader_source)),
141 })?;
142
143 let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
144 label: Some("text_bgl"),
145 entries: &[
146 BindGroupLayoutEntry {
147 binding: 0,
148 visibility: ShaderStageFlags::VERTEX,
149 ty: BindingType::Buffer {
150 ty: khora_core::renderer::api::command::BufferBindingType::Uniform,
151 has_dynamic_offset: false,
152 min_binding_size: None,
153 },
154 },
155 BindGroupLayoutEntry {
156 binding: 1,
157 visibility: ShaderStageFlags::FRAGMENT,
158 ty: BindingType::Texture {
159 sample_type: khora_core::renderer::api::command::TextureSampleType::Float {
160 filterable: true,
161 },
162 view_dimension:
163 khora_core::renderer::api::command::TextureViewDimension::D2,
164 multisampled: false,
165 },
166 },
167 BindGroupLayoutEntry {
168 binding: 2,
169 visibility: ShaderStageFlags::FRAGMENT,
170 ty: BindingType::Sampler(
171 khora_core::renderer::api::command::SamplerBindingType::Filtering,
172 ),
173 },
174 ],
175 })?;
176
177 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
178 label: Some(Cow::Borrowed("text_pipeline_layout")),
179 bind_group_layouts: &[bgl],
180 })?;
181
182 let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
184 label: Some(Cow::Borrowed("text_pipeline")),
185 layout: Some(pipeline_layout),
186 vertex_shader_module: shader_module,
187 vertex_entry_point: Cow::Borrowed("vs_main"),
188 fragment_shader_module: Some(shader_module),
189 fragment_entry_point: Some(Cow::Borrowed("fs_main")),
190 vertex_buffers_layout: Cow::Owned(vec![
191 khora_core::renderer::api::pipeline::VertexBufferLayoutDescriptor {
192 array_stride: std::mem::size_of::<TextVertex>() as u64,
193 step_mode: khora_core::renderer::api::pipeline::VertexStepMode::Vertex,
194 attributes: Cow::Owned(vec![
195 khora_core::renderer::api::pipeline::VertexAttributeDescriptor {
196 format: khora_core::renderer::api::pipeline::VertexFormat::Float32x2,
197 offset: 0,
198 shader_location: 0,
199 },
200 khora_core::renderer::api::pipeline::VertexAttributeDescriptor {
201 format: khora_core::renderer::api::pipeline::VertexFormat::Float32x2,
202 offset: 8,
203 shader_location: 1,
204 },
205 khora_core::renderer::api::pipeline::VertexAttributeDescriptor {
206 format: khora_core::renderer::api::pipeline::VertexFormat::Float32x4,
207 offset: 16,
208 shader_location: 2,
209 },
210 ]),
211 },
212 ]),
213 primitive_state: PrimitiveStateDescriptor {
214 topology: PrimitiveTopology::TriangleList,
215 ..Default::default()
216 },
217 depth_stencil_state: None,
218 color_target_states: Cow::Owned(vec![ColorTargetStateDescriptor {
219 format: device
220 .get_surface_format()
221 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
222 blend: Some(khora_core::renderer::api::pipeline::BlendStateDescriptor {
223 color: khora_core::renderer::api::pipeline::BlendComponentDescriptor {
224 src_factor: khora_core::renderer::api::pipeline::BlendFactor::SrcAlpha,
225 dst_factor:
226 khora_core::renderer::api::pipeline::BlendFactor::OneMinusSrcAlpha,
227 operation: khora_core::renderer::api::pipeline::BlendOperation::Add,
228 },
229 alpha: khora_core::renderer::api::pipeline::BlendComponentDescriptor {
230 src_factor: khora_core::renderer::api::pipeline::BlendFactor::One,
231 dst_factor: khora_core::renderer::api::pipeline::BlendFactor::Zero,
232 operation: khora_core::renderer::api::pipeline::BlendOperation::Add,
233 },
234 }),
235 write_mask: ColorWrites::ALL,
236 }]),
237 multisample_state: MultisampleStateDescriptor {
238 count: SampleCount::X1,
239 mask: !0,
240 alpha_to_coverage_enabled: false,
241 },
242 })?;
243
244 let atlas = TextureAtlas::new(device, atlas_size, TextureFormat::R8Unorm, "text_atlas")?;
246
247 let sampler = device.create_sampler(&SamplerDescriptor {
248 label: Some(Cow::Borrowed("text_sampler")),
249 address_mode_u: AddressMode::ClampToEdge,
250 address_mode_v: AddressMode::ClampToEdge,
251 address_mode_w: AddressMode::ClampToEdge,
252 mag_filter: FilterMode::Linear,
253 min_filter: FilterMode::Linear,
254 mipmap_filter: MipmapFilterMode::Nearest,
255 lod_min_clamp: 0.0,
256 lod_max_clamp: 32.0,
257 compare: None,
258 anisotropy_clamp: 1,
259 border_color: None,
260 })?;
261
262 let uniform_buffer = device.create_buffer(&BufferDescriptor {
266 label: Some(Cow::Borrowed("text_uniforms")),
267 size: 64, usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
269 mapped_at_creation: false,
270 })?;
271
272 let bind_group = device.create_bind_group(&BindGroupDescriptor {
273 label: Some("text_bg"),
274 layout: bgl,
275 entries: &[
276 BindGroupEntry {
277 binding: 0,
278 resource: BindingResource::Buffer(BufferBinding {
279 buffer: uniform_buffer,
280 offset: 0,
281 size: None,
282 }),
283 _phantom: std::marker::PhantomData,
284 },
285 BindGroupEntry {
286 binding: 1,
287 resource: BindingResource::TextureView(atlas.view()),
288 _phantom: std::marker::PhantomData,
289 },
290 BindGroupEntry {
291 binding: 2,
292 resource: BindingResource::Sampler(sampler),
293 _phantom: std::marker::PhantomData,
294 },
295 ],
296 })?;
297
298 let vertex_buffer = device.create_buffer(&BufferDescriptor {
299 label: Some(Cow::Borrowed("text_vertices")),
300 size: 1024 * 64, usage: BufferUsage::VERTEX | BufferUsage::COPY_DST,
302 mapped_at_creation: false,
303 })?;
304
305 let index_buffer = device.create_buffer(&BufferDescriptor {
306 label: Some(Cow::Borrowed("text_indices")),
307 size: 1024 * 32, usage: BufferUsage::INDEX | BufferUsage::COPY_DST,
309 mapped_at_creation: false,
310 })?;
311
312 Ok(TextGpuResources {
313 pipeline,
314 bind_group_layout: bgl,
315 sampler,
316 atlas,
317 bind_group,
318 vertex_buffer,
319 index_buffer,
320 })
321 }
322}
323
324impl TextRenderer for StandardTextRenderer {
325 fn layout_text(
326 &self,
327 text: &str,
328 font: &Handle<Font>,
329 font_id: AssetUUID,
330 font_size: f32,
331 _max_width: Option<f32>,
332 ) -> Box<dyn TextLayout> {
333 let mut glyph_positions = Vec::new();
334 let mut cursor_x = 0.0;
335
336 let char_width = font_size * 0.6;
337 let char_height = font_size;
338
339 for c in text.chars() {
340 glyph_positions.push((c as u32, Vec2::new(cursor_x, 0.0)));
341 cursor_x += char_width;
342 }
343
344 Box::new(StandardTextLayout {
345 size: Vec2::new(cursor_x, char_height),
346 glyph_positions,
347 font_handle: font.clone(),
348 font_uuid: font_id,
349 font_size,
350 })
351 }
352
353 fn queue_text(&self, layout: &dyn TextLayout, pos: Vec2, color: Vec4, _z_index: i32) {
354 if let Some(std_layout) = layout.as_any().downcast_ref::<StandardTextLayout>() {
355 let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
356 queue.push(QueuedText {
357 layout: Arc::new(StandardTextLayout {
358 size: std_layout.size,
359 glyph_positions: std_layout.glyph_positions.clone(),
360 font_handle: std_layout.font_handle.clone(),
361 font_uuid: std_layout.font_uuid,
362 font_size: std_layout.font_size,
363 }),
364 pos,
365 color,
366 });
367 }
368 }
369
370 fn flush(
371 &self,
372 device: &dyn GraphicsDevice,
373 encoder: &mut dyn CommandEncoder,
374 color_target: &TextureViewId,
375 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
376 let mut resources_lock = self
377 .gpu_resources
378 .lock()
379 .map_err(|_| "TextRenderer::flush: gpu_resources lock poisoned")?;
380 if resources_lock.is_none() {
381 *resources_lock = Some(self.init_resources(device)?);
382 }
383 let res = resources_lock.as_mut().unwrap();
384
385 let mut queue = self
386 .queue
387 .lock()
388 .map_err(|_| "TextRenderer::flush: queue lock poisoned")?;
389 if queue.is_empty() {
390 return Ok(());
391 }
392
393 let mut vertices = Vec::new();
395 let mut indices = Vec::new();
396 let mut vert_offset = 0;
397
398 for item in queue.iter() {
399 let font_uuid = item.layout.font_uuid;
400 let font_size_fixed = (item.layout.font_size * 10.0) as u32;
401
402 for (c_u32, g_pos) in &item.layout.glyph_positions {
403 let c = char::from_u32(*c_u32).unwrap_or('?');
404 let cache_key = (font_uuid, c, font_size_fixed);
405
406 let mut cache = self
408 .glyph_cache
409 .lock()
410 .map_err(|_| "TextRenderer::flush: glyph cache lock poisoned")?;
411 let glyph = if let Some(g) = cache.get(&cache_key) {
412 *g
413 } else if let Some((w, h, pixels)) = self.rasterize_glyph(c) {
414 if let Some(rect) = res.atlas.allocate_and_upload(device, w, h, &pixels, 1) {
415 let g = GlyphData {
416 uv_min: rect.min,
417 uv_max: rect.max,
418 size: Vec2::new(w as f32, h as f32),
419 };
420 cache.insert(cache_key, g);
421 g
422 } else {
423 continue; }
425 } else {
426 continue; };
428
429 let p = item.pos + *g_pos;
430 let s = item.layout.font_size;
431 let uv_min = glyph.uv_min;
432 let uv_max = glyph.uv_max;
433
434 vertices.push(TextVertex {
436 pos: p.to_array(),
437 uv: [uv_min.x, uv_min.y],
438 color: item.color.to_array(),
439 });
440 vertices.push(TextVertex {
441 pos: (p + Vec2::new(s * 0.6, 0.0)).to_array(),
442 uv: [uv_max.x, uv_min.y],
443 color: item.color.to_array(),
444 });
445 vertices.push(TextVertex {
446 pos: (p + Vec2::new(s * 0.6, s)).to_array(),
447 uv: [uv_max.x, uv_max.y],
448 color: item.color.to_array(),
449 });
450 vertices.push(TextVertex {
451 pos: (p + Vec2::new(0.0, s)).to_array(),
452 uv: [uv_min.x, uv_max.y],
453 color: item.color.to_array(),
454 });
455
456 indices.push(vert_offset);
457 indices.push(vert_offset + 1);
458 indices.push(vert_offset + 2);
459 indices.push(vert_offset);
460 indices.push(vert_offset + 2);
461 indices.push(vert_offset + 3);
462
463 vert_offset += 4;
464 }
465 }
466
467 device.write_buffer(res.vertex_buffer, 0, bytemuck::cast_slice(&vertices))?;
469 device.write_buffer(res.index_buffer, 0, bytemuck::cast_slice(&indices))?;
470
471 let attachment = RenderPassColorAttachment {
473 view: color_target,
474 resolve_target: None,
475 ops: Operations {
476 load: LoadOp::<LinearRgba>::Load,
477 store: StoreOp::Store,
478 },
479 base_array_layer: 0,
480 base_mip_level: 0,
481 };
482
483 let attachments = [attachment];
484 let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
485 label: Some("text_flush_pass"),
486 color_attachments: &attachments,
487 depth_stencil_attachment: None,
488 });
489
490 pass.set_pipeline(&res.pipeline);
491 pass.set_bind_group(0, &res.bind_group, &[]);
492 pass.set_vertex_buffer(0, &res.vertex_buffer, 0);
493 pass.set_index_buffer(&res.index_buffer, 0, IndexFormat::Uint16);
494 pass.draw_indexed(0..(indices.len() as u32), 0, 0..1);
495
496 drop(pass);
497
498 queue.clear();
499 Ok(())
500 }
501}