dama 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +227 -0
  4. data/dama-logo.svg +91 -0
  5. data/exe/dama +4 -0
  6. data/ext/dama_native/.cargo/config.toml +3 -0
  7. data/ext/dama_native/Cargo.lock +3575 -0
  8. data/ext/dama_native/Cargo.toml +39 -0
  9. data/ext/dama_native/extconf.rb +72 -0
  10. data/ext/dama_native/src/audio.rs +134 -0
  11. data/ext/dama_native/src/engine.rs +339 -0
  12. data/ext/dama_native/src/lib.rs +396 -0
  13. data/ext/dama_native/src/renderer/screenshot.rs +84 -0
  14. data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
  15. data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
  16. data/ext/dama_native/src/renderer.rs +563 -0
  17. data/ext/dama_native/src/window.rs +255 -0
  18. data/lib/dama/animation.rb +66 -0
  19. data/lib/dama/asset_cache.rb +56 -0
  20. data/lib/dama/audio.rb +47 -0
  21. data/lib/dama/auto_loader.rb +54 -0
  22. data/lib/dama/backend/base.rb +137 -0
  23. data/lib/dama/backend/native/ffi_bindings.rb +122 -0
  24. data/lib/dama/backend/native.rb +191 -0
  25. data/lib/dama/backend/web.rb +199 -0
  26. data/lib/dama/backend.rb +13 -0
  27. data/lib/dama/camera.rb +68 -0
  28. data/lib/dama/cli/new_project.rb +112 -0
  29. data/lib/dama/cli/release.rb +45 -0
  30. data/lib/dama/cli.rb +22 -0
  31. data/lib/dama/colors.rb +30 -0
  32. data/lib/dama/command_buffer.rb +83 -0
  33. data/lib/dama/component/attribute_definition.rb +13 -0
  34. data/lib/dama/component/attribute_set.rb +32 -0
  35. data/lib/dama/component.rb +28 -0
  36. data/lib/dama/configuration.rb +18 -0
  37. data/lib/dama/debug/frame_controller.rb +35 -0
  38. data/lib/dama/debug/screenshot_tool.rb +19 -0
  39. data/lib/dama/debug.rb +4 -0
  40. data/lib/dama/event_bus.rb +47 -0
  41. data/lib/dama/game/builder.rb +31 -0
  42. data/lib/dama/game/loop.rb +44 -0
  43. data/lib/dama/game.rb +88 -0
  44. data/lib/dama/geometry/circle.rb +28 -0
  45. data/lib/dama/geometry/rect.rb +16 -0
  46. data/lib/dama/geometry/sprite.rb +18 -0
  47. data/lib/dama/geometry/triangle.rb +13 -0
  48. data/lib/dama/geometry.rb +4 -0
  49. data/lib/dama/input/keyboard_state.rb +44 -0
  50. data/lib/dama/input/mouse_state.rb +45 -0
  51. data/lib/dama/input.rb +38 -0
  52. data/lib/dama/keys.rb +67 -0
  53. data/lib/dama/node/component_slot.rb +18 -0
  54. data/lib/dama/node/draw_context.rb +96 -0
  55. data/lib/dama/node.rb +139 -0
  56. data/lib/dama/physics/body.rb +57 -0
  57. data/lib/dama/physics/collider.rb +152 -0
  58. data/lib/dama/physics/collision.rb +15 -0
  59. data/lib/dama/physics/world.rb +125 -0
  60. data/lib/dama/physics.rb +4 -0
  61. data/lib/dama/registry/class_resolver.rb +48 -0
  62. data/lib/dama/registry.rb +21 -0
  63. data/lib/dama/release/archiver.rb +100 -0
  64. data/lib/dama/release/defaults/icon.icns +0 -0
  65. data/lib/dama/release/defaults/icon.ico +0 -0
  66. data/lib/dama/release/defaults/icon.png +0 -0
  67. data/lib/dama/release/dylib_relinker.rb +95 -0
  68. data/lib/dama/release/game_file_copier.rb +35 -0
  69. data/lib/dama/release/game_metadata.rb +61 -0
  70. data/lib/dama/release/icon_provider.rb +36 -0
  71. data/lib/dama/release/native_builder.rb +44 -0
  72. data/lib/dama/release/packager/linux.rb +62 -0
  73. data/lib/dama/release/packager/macos.rb +99 -0
  74. data/lib/dama/release/packager/web.rb +32 -0
  75. data/lib/dama/release/packager/windows.rb +61 -0
  76. data/lib/dama/release/packager.rb +9 -0
  77. data/lib/dama/release/platform_detector.rb +23 -0
  78. data/lib/dama/release/ruby_bundler.rb +163 -0
  79. data/lib/dama/release/stdlib_trimmer.rb +133 -0
  80. data/lib/dama/release/template_renderer.rb +40 -0
  81. data/lib/dama/release/templates/info_plist.xml.erb +19 -0
  82. data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
  83. data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
  84. data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
  85. data/lib/dama/release.rb +7 -0
  86. data/lib/dama/scene/composer.rb +65 -0
  87. data/lib/dama/scene.rb +233 -0
  88. data/lib/dama/scene_graph/class_index.rb +26 -0
  89. data/lib/dama/scene_graph/group_node.rb +27 -0
  90. data/lib/dama/scene_graph/instance_node.rb +30 -0
  91. data/lib/dama/scene_graph/path_selector.rb +25 -0
  92. data/lib/dama/scene_graph/query.rb +34 -0
  93. data/lib/dama/scene_graph/tag_index.rb +26 -0
  94. data/lib/dama/scene_graph/tree.rb +65 -0
  95. data/lib/dama/scene_graph.rb +4 -0
  96. data/lib/dama/sprite_sheet.rb +36 -0
  97. data/lib/dama/tween/easing.rb +31 -0
  98. data/lib/dama/tween/lerp.rb +35 -0
  99. data/lib/dama/tween/manager.rb +28 -0
  100. data/lib/dama/tween.rb +4 -0
  101. data/lib/dama/version.rb +3 -0
  102. data/lib/dama/vertex_batch.rb +35 -0
  103. data/lib/dama/web/entry.rb +79 -0
  104. data/lib/dama/web/static/index.html +142 -0
  105. data/lib/dama/web_builder.rb +232 -0
  106. data/lib/dama.rb +42 -0
  107. metadata +198 -0
@@ -0,0 +1,507 @@
1
+ use std::collections::HashMap;
2
+ use wgpu::util::DeviceExt;
3
+
4
+ /// A vertex with 2D position, RGBA color, and UV texture coordinates.
5
+ /// Used for all rendering: shapes (UV=0,0 with white texture) and sprites.
6
+ #[repr(C)]
7
+ #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
8
+ pub struct Vertex {
9
+ pub position: [f32; 2],
10
+ pub color: [f32; 4],
11
+ pub uv: [f32; 2],
12
+ }
13
+
14
+ impl Vertex {
15
+ const ATTRIBS: [wgpu::VertexAttribute; 3] =
16
+ wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4, 2 => Float32x2];
17
+
18
+ fn layout() -> wgpu::VertexBufferLayout<'static> {
19
+ wgpu::VertexBufferLayout {
20
+ array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
21
+ step_mode: wgpu::VertexStepMode::Vertex,
22
+ attributes: &Self::ATTRIBS,
23
+ }
24
+ }
25
+ }
26
+
27
+ /// Uniform data passed to custom shaders (Group 1, Binding 0).
28
+ #[repr(C)]
29
+ #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
30
+ struct Uniforms {
31
+ time: f32,
32
+ _padding: [f32; 3], // Pad to 16 bytes (wgpu minimum).
33
+ }
34
+
35
+ /// Stored GPU texture with its bind group for rendering.
36
+ pub struct GpuTexture {
37
+ #[allow(dead_code)]
38
+ texture: wgpu::Texture,
39
+ bind_group: wgpu::BindGroup,
40
+ }
41
+
42
+ /// A cached custom shader: WGSL source + lazily compiled pipeline.
43
+ struct ShaderEntry {
44
+ source: String,
45
+ pipeline: Option<wgpu::RenderPipeline>,
46
+ }
47
+
48
+ /// Renders colored and textured triangles with optional custom shaders.
49
+ ///
50
+ /// The default shader samples a texture and multiplies by vertex color:
51
+ /// output = textureSample(tex, sampler, uv) * color
52
+ ///
53
+ /// Custom shaders receive the same vertex data plus a uniform buffer
54
+ /// with `time: f32` for animated effects.
55
+ pub struct ShapeRenderer {
56
+ /// Default pipeline (handle = 0).
57
+ pipeline: wgpu::RenderPipeline,
58
+ /// Bind group layout for textures (Group 0: texture + sampler).
59
+ texture_bind_group_layout: wgpu::BindGroupLayout,
60
+ /// Bind group layout for uniforms (Group 1: time).
61
+ uniform_bind_group_layout: wgpu::BindGroupLayout,
62
+ sampler: wgpu::Sampler,
63
+ /// The default 1x1 white texture bind group (handle = 0).
64
+ default_bind_group: wgpu::BindGroup,
65
+ /// Uniform buffer for time + bind group.
66
+ uniform_buffer: wgpu::Buffer,
67
+ uniform_bind_group: wgpu::BindGroup,
68
+ /// Surface format for lazy pipeline creation.
69
+ surface_format: wgpu::TextureFormat,
70
+ /// User-loaded textures keyed by handle.
71
+ textures: HashMap<u64, GpuTexture>,
72
+ next_texture_handle: u64,
73
+ /// Custom shaders keyed by handle. Pipeline created lazily on first use.
74
+ shaders: HashMap<u64, ShaderEntry>,
75
+ next_shader_handle: u64,
76
+ }
77
+
78
+ impl ShapeRenderer {
79
+ pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
80
+ // --- Bind group layouts ---
81
+
82
+ let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
83
+ label: Some("texture_bind_group_layout"),
84
+ entries: &[
85
+ wgpu::BindGroupLayoutEntry {
86
+ binding: 0,
87
+ visibility: wgpu::ShaderStages::FRAGMENT,
88
+ ty: wgpu::BindingType::Texture {
89
+ multisampled: false,
90
+ view_dimension: wgpu::TextureViewDimension::D2,
91
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
92
+ },
93
+ count: None,
94
+ },
95
+ wgpu::BindGroupLayoutEntry {
96
+ binding: 1,
97
+ visibility: wgpu::ShaderStages::FRAGMENT,
98
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
99
+ count: None,
100
+ },
101
+ ],
102
+ });
103
+
104
+ let uniform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
105
+ label: Some("uniform_bind_group_layout"),
106
+ entries: &[
107
+ wgpu::BindGroupLayoutEntry {
108
+ binding: 0,
109
+ visibility: wgpu::ShaderStages::FRAGMENT,
110
+ ty: wgpu::BindingType::Buffer {
111
+ ty: wgpu::BufferBindingType::Uniform,
112
+ has_dynamic_offset: false,
113
+ min_binding_size: None,
114
+ },
115
+ count: None,
116
+ },
117
+ ],
118
+ });
119
+
120
+ // --- Uniform buffer ---
121
+
122
+ let uniforms = Uniforms { time: 0.0, _padding: [0.0; 3] };
123
+ let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
124
+ label: Some("uniform_buffer"),
125
+ contents: bytemuck::cast_slice(&[uniforms]),
126
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
127
+ });
128
+
129
+ let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
130
+ label: Some("uniform_bind_group"),
131
+ layout: &uniform_bind_group_layout,
132
+ entries: &[wgpu::BindGroupEntry {
133
+ binding: 0,
134
+ resource: uniform_buffer.as_entire_binding(),
135
+ }],
136
+ });
137
+
138
+ // --- Default pipeline (uses both bind group layouts) ---
139
+
140
+ let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
141
+ label: Some("shape_shader"),
142
+ source: wgpu::ShaderSource::Wgsl(DEFAULT_SHADER.into()),
143
+ });
144
+
145
+ let pipeline = Self::create_pipeline(
146
+ device, &shader, format,
147
+ &texture_bind_group_layout, &uniform_bind_group_layout,
148
+ );
149
+
150
+ // --- Sampler + default white texture ---
151
+
152
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
153
+ label: Some("sprite_sampler"),
154
+ mag_filter: wgpu::FilterMode::Nearest,
155
+ min_filter: wgpu::FilterMode::Nearest,
156
+ ..Default::default()
157
+ });
158
+
159
+ let (default_bind_group, _white_tex) = Self::create_white_pixel(
160
+ device, queue, &texture_bind_group_layout, &sampler,
161
+ );
162
+
163
+ Self {
164
+ pipeline,
165
+ texture_bind_group_layout,
166
+ uniform_bind_group_layout,
167
+ sampler,
168
+ default_bind_group,
169
+ uniform_buffer,
170
+ uniform_bind_group,
171
+ surface_format: format,
172
+ textures: HashMap::new(),
173
+ next_texture_handle: 1,
174
+ shaders: HashMap::new(),
175
+ next_shader_handle: 1,
176
+ }
177
+ }
178
+
179
+ /// Update the time uniform. Call once per frame.
180
+ pub fn update_time(&self, queue: &wgpu::Queue, time: f32) {
181
+ let uniforms = Uniforms { time, _padding: [0.0; 3] };
182
+ queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
183
+ }
184
+
185
+ // --- Texture management ---
186
+
187
+ pub fn load_texture(
188
+ &mut self,
189
+ device: &wgpu::Device,
190
+ queue: &wgpu::Queue,
191
+ data: &[u8],
192
+ ) -> Result<u64, String> {
193
+ let img = image::load_from_memory(data)
194
+ .map_err(|e| format!("Failed to decode image: {e}"))?
195
+ .to_rgba8();
196
+
197
+ let (width, height) = img.dimensions();
198
+
199
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
200
+ label: Some("user_texture"),
201
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
202
+ mip_level_count: 1,
203
+ sample_count: 1,
204
+ dimension: wgpu::TextureDimension::D2,
205
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
206
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
207
+ view_formats: &[],
208
+ });
209
+
210
+ queue.write_texture(
211
+ wgpu::TexelCopyTextureInfo {
212
+ texture: &texture, mip_level: 0,
213
+ origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All,
214
+ },
215
+ &img,
216
+ wgpu::TexelCopyBufferLayout {
217
+ offset: 0, bytes_per_row: Some(4 * width), rows_per_image: Some(height),
218
+ },
219
+ wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
220
+ );
221
+
222
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
223
+ let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
224
+ label: Some("user_texture_bind_group"),
225
+ layout: &self.texture_bind_group_layout,
226
+ entries: &[
227
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
228
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
229
+ ],
230
+ });
231
+
232
+ let handle = self.next_texture_handle;
233
+ self.next_texture_handle += 1;
234
+ self.textures.insert(handle, GpuTexture { texture, bind_group });
235
+ Ok(handle)
236
+ }
237
+
238
+ pub fn unload_texture(&mut self, handle: u64) {
239
+ self.textures.remove(&handle);
240
+ }
241
+
242
+ pub fn bind_group_for(&self, handle: u64) -> &wgpu::BindGroup {
243
+ self.textures
244
+ .get(&handle)
245
+ .map(|t| &t.bind_group)
246
+ .unwrap_or(&self.default_bind_group)
247
+ }
248
+
249
+ // --- Shader management ---
250
+
251
+ /// Store a custom WGSL fragment shader. Returns a handle.
252
+ /// The pipeline is created lazily on first render.
253
+ pub fn load_shader(&mut self, source: &str) -> u64 {
254
+ let handle = self.next_shader_handle;
255
+ self.next_shader_handle += 1;
256
+ self.shaders.insert(handle, ShaderEntry {
257
+ source: source.to_string(),
258
+ pipeline: None,
259
+ });
260
+ handle
261
+ }
262
+
263
+ pub fn unload_shader(&mut self, handle: u64) {
264
+ self.shaders.remove(&handle);
265
+ }
266
+
267
+ pub fn shader_count(&self) -> usize {
268
+ self.shaders.len()
269
+ }
270
+
271
+ // --- Rendering ---
272
+
273
+ /// Render a batch of vertices with a specific texture and shader.
274
+ /// shader_handle = 0 uses the default pipeline.
275
+ pub fn render_batch(
276
+ &mut self,
277
+ device: &wgpu::Device,
278
+ render_pass: &mut wgpu::RenderPass<'_>,
279
+ vertices: &[Vertex],
280
+ texture_handle: u64,
281
+ shader_handle: u64,
282
+ ) {
283
+ if vertices.is_empty() {
284
+ return;
285
+ }
286
+
287
+ let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
288
+ label: Some("shape_vertex_buffer"),
289
+ contents: bytemuck::cast_slice(vertices),
290
+ usage: wgpu::BufferUsages::VERTEX,
291
+ });
292
+
293
+ // Select pipeline: default or custom shader.
294
+ self.ensure_shader_pipeline(device, shader_handle);
295
+ let pipeline = self.pipeline_for(shader_handle);
296
+
297
+ let texture_bind_group = self.bind_group_for(texture_handle);
298
+
299
+ render_pass.set_pipeline(pipeline);
300
+ render_pass.set_bind_group(0, texture_bind_group, &[]);
301
+ render_pass.set_bind_group(1, &self.uniform_bind_group, &[]);
302
+ render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
303
+ render_pass.draw(0..vertices.len() as u32, 0..1);
304
+ }
305
+
306
+ // --- Private helpers ---
307
+
308
+ /// Lazily compile the shader pipeline if not yet created.
309
+ fn ensure_shader_pipeline(&mut self, device: &wgpu::Device, shader_handle: u64) {
310
+ if shader_handle == 0 { return; }
311
+
312
+ match self.shaders.get(&shader_handle) {
313
+ Some(e) if e.pipeline.is_some() => return,
314
+ Some(_) => {},
315
+ None => return,
316
+ }
317
+
318
+ // Build full WGSL by prepending the engine preamble to the user's fragment shader.
319
+ let source = self.shaders.get(&shader_handle).unwrap().source.clone();
320
+ let full_wgsl = format!("{SHADER_PREAMBLE}\n{source}");
321
+
322
+ let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
323
+ label: Some("custom_shader"),
324
+ source: wgpu::ShaderSource::Wgsl(full_wgsl.into()),
325
+ });
326
+
327
+ let pipeline = Self::create_pipeline(
328
+ device, &module, self.surface_format,
329
+ &self.texture_bind_group_layout, &self.uniform_bind_group_layout,
330
+ );
331
+
332
+ if let Some(entry) = self.shaders.get_mut(&shader_handle) {
333
+ entry.pipeline = Some(pipeline);
334
+ }
335
+ }
336
+
337
+ fn pipeline_for(&self, shader_handle: u64) -> &wgpu::RenderPipeline {
338
+ if shader_handle == 0 {
339
+ return &self.pipeline;
340
+ }
341
+
342
+ self.shaders
343
+ .get(&shader_handle)
344
+ .and_then(|e| e.pipeline.as_ref())
345
+ .unwrap_or(&self.pipeline)
346
+ }
347
+
348
+ fn create_pipeline(
349
+ device: &wgpu::Device,
350
+ shader: &wgpu::ShaderModule,
351
+ format: wgpu::TextureFormat,
352
+ texture_layout: &wgpu::BindGroupLayout,
353
+ uniform_layout: &wgpu::BindGroupLayout,
354
+ ) -> wgpu::RenderPipeline {
355
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
356
+ label: Some("shape_pipeline_layout"),
357
+ bind_group_layouts: &[texture_layout, uniform_layout],
358
+ immediate_size: 0,
359
+ });
360
+
361
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
362
+ label: Some("shape_pipeline"),
363
+ layout: Some(&pipeline_layout),
364
+ vertex: wgpu::VertexState {
365
+ module: shader,
366
+ entry_point: Some("vs_main"),
367
+ buffers: &[Vertex::layout()],
368
+ compilation_options: Default::default(),
369
+ },
370
+ fragment: Some(wgpu::FragmentState {
371
+ module: shader,
372
+ entry_point: Some("fs_main"),
373
+ targets: &[Some(wgpu::ColorTargetState {
374
+ format,
375
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
376
+ write_mask: wgpu::ColorWrites::ALL,
377
+ })],
378
+ compilation_options: Default::default(),
379
+ }),
380
+ primitive: wgpu::PrimitiveState {
381
+ topology: wgpu::PrimitiveTopology::TriangleList,
382
+ strip_index_format: None,
383
+ front_face: wgpu::FrontFace::Ccw,
384
+ cull_mode: None,
385
+ polygon_mode: wgpu::PolygonMode::Fill,
386
+ unclipped_depth: false,
387
+ conservative: false,
388
+ },
389
+ depth_stencil: None,
390
+ multisample: wgpu::MultisampleState::default(),
391
+ multiview_mask: None,
392
+ cache: None,
393
+ })
394
+ }
395
+
396
+ fn create_white_pixel(
397
+ device: &wgpu::Device,
398
+ queue: &wgpu::Queue,
399
+ layout: &wgpu::BindGroupLayout,
400
+ sampler: &wgpu::Sampler,
401
+ ) -> (wgpu::BindGroup, wgpu::Texture) {
402
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
403
+ label: Some("white_pixel"),
404
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
405
+ mip_level_count: 1,
406
+ sample_count: 1,
407
+ dimension: wgpu::TextureDimension::D2,
408
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
409
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
410
+ view_formats: &[],
411
+ });
412
+
413
+ queue.write_texture(
414
+ wgpu::TexelCopyTextureInfo {
415
+ texture: &texture, mip_level: 0,
416
+ origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All,
417
+ },
418
+ &[255u8, 255, 255, 255],
419
+ wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4), rows_per_image: Some(1) },
420
+ wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
421
+ );
422
+
423
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
424
+ let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
425
+ label: Some("default_texture_bind_group"),
426
+ layout,
427
+ entries: &[
428
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
429
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) },
430
+ ],
431
+ });
432
+
433
+ (bind_group, texture)
434
+ }
435
+ }
436
+
437
+ /// Preamble prepended to custom fragment shaders.
438
+ /// Provides VertexOutput struct, texture/sampler bindings, and uniform buffer.
439
+ const SHADER_PREAMBLE: &str = r#"
440
+ struct VertexInput {
441
+ @location(0) position: vec2<f32>,
442
+ @location(1) color: vec4<f32>,
443
+ @location(2) uv: vec2<f32>,
444
+ };
445
+
446
+ struct VertexOutput {
447
+ @builtin(position) clip_position: vec4<f32>,
448
+ @location(0) color: vec4<f32>,
449
+ @location(1) uv: vec2<f32>,
450
+ };
451
+
452
+ @group(0) @binding(0) var t_diffuse: texture_2d<f32>;
453
+ @group(0) @binding(1) var s_diffuse: sampler;
454
+
455
+ struct Uniforms {
456
+ time: f32,
457
+ };
458
+ @group(1) @binding(0) var<uniform> u: Uniforms;
459
+
460
+ @vertex
461
+ fn vs_main(in: VertexInput) -> VertexOutput {
462
+ var out: VertexOutput;
463
+ out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
464
+ out.color = in.color;
465
+ out.uv = in.uv;
466
+ return out;
467
+ }
468
+ "#;
469
+
470
+ /// Default shader: the same as before but with the uniform bind group present
471
+ /// (Group 1 exists but is unused by the default fragment shader).
472
+ const DEFAULT_SHADER: &str = r#"
473
+ struct VertexInput {
474
+ @location(0) position: vec2<f32>,
475
+ @location(1) color: vec4<f32>,
476
+ @location(2) uv: vec2<f32>,
477
+ };
478
+
479
+ struct VertexOutput {
480
+ @builtin(position) clip_position: vec4<f32>,
481
+ @location(0) color: vec4<f32>,
482
+ @location(1) uv: vec2<f32>,
483
+ };
484
+
485
+ @group(0) @binding(0) var t_diffuse: texture_2d<f32>;
486
+ @group(0) @binding(1) var s_diffuse: sampler;
487
+
488
+ struct Uniforms {
489
+ time: f32,
490
+ };
491
+ @group(1) @binding(0) var<uniform> u: Uniforms;
492
+
493
+ @vertex
494
+ fn vs_main(in: VertexInput) -> VertexOutput {
495
+ var out: VertexOutput;
496
+ out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
497
+ out.color = in.color;
498
+ out.uv = in.uv;
499
+ return out;
500
+ }
501
+
502
+ @fragment
503
+ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
504
+ let tex_color = textureSample(t_diffuse, s_diffuse, in.uv);
505
+ return tex_color * in.color;
506
+ }
507
+ "#;
@@ -0,0 +1,192 @@
1
+ use glyphon::{
2
+ Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
3
+ TextArea, TextAtlas, TextBounds, TextRenderer as GlyphonRenderer, Viewport,
4
+ };
5
+
6
+ /// A queued text draw command, collected between begin_frame and end_frame.
7
+ pub struct TextCommand {
8
+ pub text: String,
9
+ pub x: f32,
10
+ pub y: f32,
11
+ pub size: f32,
12
+ pub r: u8,
13
+ pub g: u8,
14
+ pub b: u8,
15
+ pub a: u8,
16
+ /// Optional custom font family name. None = SansSerif (default).
17
+ pub font_family: Option<String>,
18
+ }
19
+
20
+ /// Wraps glyphon for GPU text rendering. Integrates as middleware
21
+ /// into an existing wgpu render pass (renders after shapes).
22
+ pub struct TextRenderer {
23
+ font_system: FontSystem,
24
+ swash_cache: SwashCache,
25
+ #[allow(dead_code)]
26
+ cache: Cache,
27
+ viewport: Viewport,
28
+ atlas: TextAtlas,
29
+ renderer: GlyphonRenderer,
30
+ width: u32,
31
+ height: u32,
32
+ pending_texts: Vec<TextCommand>,
33
+ /// Buffers created during prepare(), kept alive until after render().
34
+ prepared_buffers: Vec<Buffer>,
35
+ }
36
+
37
+ impl TextRenderer {
38
+ pub fn new(
39
+ device: &wgpu::Device,
40
+ queue: &wgpu::Queue,
41
+ format: wgpu::TextureFormat,
42
+ width: u32,
43
+ height: u32,
44
+ ) -> Self {
45
+ let font_system = FontSystem::new();
46
+
47
+ // On wasm there are no system fonts — embed a default font.
48
+ #[cfg(target_arch = "wasm32")]
49
+ let font_system = {
50
+ let mut fs = font_system;
51
+ let font_data = include_bytes!("../fonts/NotoSans-Regular.ttf").to_vec();
52
+ fs.db_mut().load_font_data(font_data);
53
+ fs
54
+ };
55
+ let swash_cache = SwashCache::new();
56
+ let cache = Cache::new(device);
57
+ let viewport = Viewport::new(device, &cache);
58
+ let mut atlas = TextAtlas::new(device, queue, &cache, format);
59
+ let renderer =
60
+ GlyphonRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
61
+
62
+ Self {
63
+ font_system,
64
+ swash_cache,
65
+ cache,
66
+ viewport,
67
+ atlas,
68
+ renderer,
69
+ width,
70
+ height,
71
+ pending_texts: Vec::new(),
72
+ prepared_buffers: Vec::new(),
73
+ }
74
+ }
75
+
76
+ /// Load a custom font file into the font system.
77
+ pub fn load_font(&mut self, data: Vec<u8>) {
78
+ self.font_system.db_mut().load_font_data(data);
79
+ }
80
+
81
+ pub fn queue_text(
82
+ &mut self, text: &str, x: f32, y: f32, size: f32,
83
+ r: f32, g: f32, b: f32, a: f32,
84
+ font_family: Option<&str>,
85
+ ) {
86
+ self.pending_texts.push(TextCommand {
87
+ text: text.to_string(),
88
+ x,
89
+ y,
90
+ size,
91
+ r: (r * 255.0) as u8,
92
+ g: (g * 255.0) as u8,
93
+ b: (b * 255.0) as u8,
94
+ a: (a * 255.0) as u8,
95
+ font_family: font_family.map(|s| s.to_string()),
96
+ });
97
+ }
98
+
99
+ /// Prepare text for rendering. Must be called before the render pass.
100
+ /// Creates glyphon Buffers, shapes text, and uploads glyphs to the atlas.
101
+ pub fn prepare(
102
+ &mut self,
103
+ device: &wgpu::Device,
104
+ queue: &wgpu::Queue,
105
+ ) -> Result<(), String> {
106
+ self.prepared_buffers.clear();
107
+
108
+ self.viewport.update(
109
+ queue,
110
+ Resolution {
111
+ width: self.width,
112
+ height: self.height,
113
+ },
114
+ );
115
+
116
+ // Create a Buffer for each pending text command.
117
+ for cmd in &self.pending_texts {
118
+ let metrics = Metrics::new(cmd.size, cmd.size * 1.2);
119
+ let mut buffer = Buffer::new(&mut self.font_system, metrics);
120
+ buffer.set_size(
121
+ &mut self.font_system,
122
+ Some(self.width as f32),
123
+ Some(self.height as f32),
124
+ );
125
+ let attrs = match &cmd.font_family {
126
+ Some(name) => Attrs::new().family(Family::Name(name)),
127
+ None => Attrs::new().family(Family::SansSerif),
128
+ };
129
+ buffer.set_text(
130
+ &mut self.font_system,
131
+ &cmd.text,
132
+ &attrs,
133
+ Shaping::Advanced,
134
+ None,
135
+ );
136
+ buffer.shape_until_scroll(&mut self.font_system, false);
137
+ self.prepared_buffers.push(buffer);
138
+ }
139
+
140
+ // Build TextArea references for all prepared buffers.
141
+ let text_areas: Vec<TextArea> = self
142
+ .prepared_buffers
143
+ .iter()
144
+ .zip(self.pending_texts.iter())
145
+ .map(|(buffer, cmd)| TextArea {
146
+ buffer,
147
+ left: cmd.x,
148
+ top: cmd.y,
149
+ scale: 1.0,
150
+ bounds: TextBounds {
151
+ left: 0,
152
+ top: 0,
153
+ right: self.width as i32,
154
+ bottom: self.height as i32,
155
+ },
156
+ default_color: Color::rgba(cmd.r, cmd.g, cmd.b, cmd.a),
157
+ custom_glyphs: &[],
158
+ })
159
+ .collect();
160
+
161
+ self.renderer
162
+ .prepare(
163
+ device,
164
+ queue,
165
+ &mut self.font_system,
166
+ &mut self.atlas,
167
+ &self.viewport,
168
+ text_areas,
169
+ &mut self.swash_cache,
170
+ )
171
+ .map_err(|e| format!("Text prepare failed: {e}"))
172
+ }
173
+
174
+ /// Render prepared text into an active render pass.
175
+ pub fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) -> Result<(), String> {
176
+ self.renderer
177
+ .render(&self.atlas, &self.viewport, pass)
178
+ .map_err(|e| format!("Text render failed: {e}"))
179
+ }
180
+
181
+ /// Clear pending text and trim the glyph atlas.
182
+ pub fn clear(&mut self) {
183
+ self.pending_texts.clear();
184
+ self.prepared_buffers.clear();
185
+ self.atlas.trim();
186
+ }
187
+
188
+ pub fn update_dimensions(&mut self, width: u32, height: u32) {
189
+ self.width = width;
190
+ self.height = height;
191
+ }
192
+ }