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,396 @@
1
+ #![allow(clippy::too_many_arguments)]
2
+
3
+ pub mod engine;
4
+ pub mod renderer;
5
+ pub mod window;
6
+
7
+ #[cfg(not(target_arch = "wasm32"))]
8
+ pub mod audio;
9
+
10
+ use engine::Engine;
11
+
12
+ // ===========================================================================
13
+ // Native FFI exports (extern "C" for Ruby FFI)
14
+ // ===========================================================================
15
+ #[cfg(not(target_arch = "wasm32"))]
16
+ pub mod native_ffi {
17
+ use super::*;
18
+ use std::ffi::{CStr, CString};
19
+ use std::os::raw::c_char;
20
+
21
+ thread_local! {
22
+ static LAST_ERROR: std::cell::RefCell<Option<CString>> = const { std::cell::RefCell::new(None) };
23
+ }
24
+
25
+ fn set_last_error(msg: &str) {
26
+ LAST_ERROR.with(|cell| { *cell.borrow_mut() = CString::new(msg).ok(); });
27
+ }
28
+
29
+ fn ok_or_err<T>(result: Result<T, String>, success_val: i32) -> i32 {
30
+ match result {
31
+ Ok(_) => success_val,
32
+ Err(e) => { set_last_error(&e); -1 }
33
+ }
34
+ }
35
+
36
+ #[unsafe(no_mangle)]
37
+ pub extern "C" fn dama_engine_init_headless(width: u32, height: u32) -> i32 {
38
+ let _ = env_logger::try_init();
39
+ ok_or_err(Engine::init_headless(width, height), 0)
40
+ }
41
+
42
+ /// # Safety
43
+ /// `title` must be a valid, non-null, null-terminated C string.
44
+ #[unsafe(no_mangle)]
45
+ pub unsafe extern "C" fn dama_engine_init(width: u32, height: u32, title: *const c_char) -> i32 {
46
+ let _ = env_logger::try_init();
47
+ let title = CStr::from_ptr(title).to_str().unwrap_or("dama");
48
+ ok_or_err(Engine::init_windowed(width, height, title), 0)
49
+ }
50
+
51
+ #[unsafe(no_mangle)]
52
+ pub extern "C" fn dama_engine_shutdown() -> i32 { ok_or_err(Engine::shutdown(), 0) }
53
+
54
+ #[unsafe(no_mangle)]
55
+ pub extern "C" fn dama_engine_poll_events() -> i32 {
56
+ let is_windowed = Engine::with(|e| Ok(e.window_state().is_some())).unwrap_or(false);
57
+ if !is_windowed { return 0; }
58
+ match Engine::pump_events() {
59
+ Ok(true) => 1, Ok(false) => 0,
60
+ Err(e) => { set_last_error(&e); -1 }
61
+ }
62
+ }
63
+
64
+ #[unsafe(no_mangle)]
65
+ pub extern "C" fn dama_engine_begin_frame() -> i32 { ok_or_err(Engine::with(|e| e.begin_frame()), 0) }
66
+
67
+ #[unsafe(no_mangle)]
68
+ pub extern "C" fn dama_engine_end_frame() -> i32 { ok_or_err(Engine::with(|e| e.end_frame()), 0) }
69
+
70
+ #[unsafe(no_mangle)]
71
+ pub extern "C" fn dama_engine_delta_time() -> f64 { Engine::with(|e| Ok(e.delta_time())).unwrap_or(0.0) }
72
+
73
+ #[unsafe(no_mangle)]
74
+ pub extern "C" fn dama_engine_frame_count() -> u64 { Engine::with(|e| Ok(e.frame_count())).unwrap_or(0) }
75
+
76
+ #[unsafe(no_mangle)]
77
+ pub extern "C" fn dama_engine_last_error() -> *const c_char {
78
+ LAST_ERROR.with(|cell| cell.borrow().as_ref().map(|s| s.as_ptr()).unwrap_or(std::ptr::null()))
79
+ }
80
+
81
+ #[unsafe(no_mangle)]
82
+ pub extern "C" fn dama_render_clear(r: f32, g: f32, b: f32, a: f32) -> i32 {
83
+ ok_or_err(Engine::with(|e| e.renderer().clear(r, g, b, a)), 0)
84
+ }
85
+
86
+ /// # Safety
87
+ /// `vertex_data` must point to at least `vertex_count * 8` valid `f32` values.
88
+ #[unsafe(no_mangle)]
89
+ pub unsafe extern "C" fn dama_render_vertices(vertex_data: *const f32, vertex_count: u32) -> i32 {
90
+ let count = vertex_count as usize;
91
+ let floats = std::slice::from_raw_parts(vertex_data, count * 8);
92
+ ok_or_err(Engine::with(|e| { e.renderer().submit_vertices(floats, count); Ok(()) }), 0)
93
+ }
94
+
95
+ /// # Safety
96
+ /// `command_data` must point to at least `float_count` valid `f32` values.
97
+ #[unsafe(no_mangle)]
98
+ pub unsafe extern "C" fn dama_render_commands(command_data: *const f32, float_count: u32) -> i32 {
99
+ let count = float_count as usize;
100
+ let commands = std::slice::from_raw_parts(command_data, count);
101
+ ok_or_err(Engine::with(|e| { e.renderer().submit_commands(commands); Ok(()) }), 0)
102
+ }
103
+
104
+ #[unsafe(no_mangle)]
105
+ pub extern "C" fn dama_render_set_texture(handle: u64) -> i32 {
106
+ ok_or_err(Engine::with(|e| { e.renderer().set_current_texture(handle); Ok(()) }), 0)
107
+ }
108
+
109
+ /// # Safety
110
+ /// `data` must point to at least `length` valid bytes of PNG image data.
111
+ #[unsafe(no_mangle)]
112
+ pub unsafe extern "C" fn dama_asset_load_texture(data: *const u8, length: u32) -> u64 {
113
+ let bytes = std::slice::from_raw_parts(data, length as usize);
114
+ match Engine::with(|e| e.renderer().load_texture(bytes)) {
115
+ Ok(handle) => handle,
116
+ Err(e) => { set_last_error(&e); 0 }
117
+ }
118
+ }
119
+
120
+ #[unsafe(no_mangle)]
121
+ pub extern "C" fn dama_asset_unload_texture(handle: u64) -> i32 {
122
+ ok_or_err(Engine::with(|e| { e.renderer().unload_texture(handle); Ok(()) }), 0)
123
+ }
124
+
125
+ // --- Shader management ---
126
+
127
+ /// # Safety
128
+ /// `source` must be a valid null-terminated C string, or null (null is handled gracefully).
129
+ #[unsafe(no_mangle)]
130
+ pub unsafe extern "C" fn dama_shader_load(source: *const c_char) -> u64 {
131
+ if source.is_null() {
132
+ set_last_error("Null shader source pointer");
133
+ return 0;
134
+ }
135
+ let source_str = match CStr::from_ptr(source).to_str() {
136
+ Ok(s) => s,
137
+ Err(e) => { set_last_error(&format!("Invalid UTF-8 in shader source: {e}")); return 0; }
138
+ };
139
+ match Engine::with(|e| e.renderer().load_shader(source_str)) {
140
+ Ok(handle) => handle,
141
+ Err(e) => { set_last_error(&e); 0 }
142
+ }
143
+ }
144
+
145
+ #[unsafe(no_mangle)]
146
+ pub extern "C" fn dama_shader_unload(handle: u64) -> i32 {
147
+ ok_or_err(Engine::with(|e| { e.renderer().unload_shader(handle); Ok(()) }), 0)
148
+ }
149
+
150
+ #[unsafe(no_mangle)]
151
+ pub extern "C" fn dama_render_set_shader(handle: u64) -> i32 {
152
+ ok_or_err(Engine::with(|e| { e.renderer().set_current_shader(handle); Ok(()) }), 0)
153
+ }
154
+
155
+ /// # Safety
156
+ /// `text` must be a valid, non-null, null-terminated C string.
157
+ #[unsafe(no_mangle)]
158
+ pub unsafe extern "C" fn dama_render_text(text: *const c_char, x: f32, y: f32, size: f32, r: f32, g: f32, b: f32, a: f32) -> i32 {
159
+ let text_str = CStr::from_ptr(text).to_str().map_err(|e| format!("Invalid UTF-8: {e}"));
160
+ match text_str {
161
+ Ok(s) => ok_or_err(Engine::with(|e| { e.renderer().draw_text(s, x, y, size, r, g, b, a, None); Ok(()) }), 0),
162
+ Err(e) => { set_last_error(&e); -1 }
163
+ }
164
+ }
165
+
166
+ /// # Safety
167
+ /// `text` and `font_family` must be valid, non-null, null-terminated C strings.
168
+ #[unsafe(no_mangle)]
169
+ pub unsafe extern "C" fn dama_render_text_with_font(
170
+ text: *const c_char, x: f32, y: f32, size: f32,
171
+ r: f32, g: f32, b: f32, a: f32,
172
+ font_family: *const c_char,
173
+ ) -> i32 {
174
+ let text_str = CStr::from_ptr(text).to_str().unwrap_or("");
175
+ let family = CStr::from_ptr(font_family).to_str().ok();
176
+ ok_or_err(Engine::with(|e| { e.renderer().draw_text(text_str, x, y, size, r, g, b, a, family); Ok(()) }), 0)
177
+ }
178
+
179
+ /// # Safety
180
+ /// `path` must be a valid, non-null, null-terminated C string pointing to a font file.
181
+ #[unsafe(no_mangle)]
182
+ pub unsafe extern "C" fn dama_font_load(path: *const c_char) -> i32 {
183
+ let path_str = CStr::from_ptr(path).to_str().unwrap_or("");
184
+ let data = match std::fs::read(path_str) {
185
+ Ok(d) => d,
186
+ Err(e) => { set_last_error(&format!("Failed to read font: {e}")); return -1; }
187
+ };
188
+ ok_or_err(Engine::with(|e| { e.renderer().load_font(data); Ok(()) }), 0)
189
+ }
190
+
191
+ #[unsafe(no_mangle)]
192
+ pub extern "C" fn dama_input_key_pressed(key_code: u32) -> i32 {
193
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.is_key_pressed(key_code)).unwrap_or(false))).unwrap_or(false) as i32
194
+ }
195
+ #[unsafe(no_mangle)]
196
+ pub extern "C" fn dama_input_key_just_pressed(key_code: u32) -> i32 {
197
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.is_key_just_pressed(key_code)).unwrap_or(false))).unwrap_or(false) as i32
198
+ }
199
+ #[unsafe(no_mangle)]
200
+ pub extern "C" fn dama_input_key_just_released(key_code: u32) -> i32 {
201
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.is_key_just_released(key_code)).unwrap_or(false))).unwrap_or(false) as i32
202
+ }
203
+ #[unsafe(no_mangle)]
204
+ pub extern "C" fn dama_input_mouse_x() -> f32 {
205
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.mouse_x()).unwrap_or(0.0))).unwrap_or(0.0)
206
+ }
207
+ #[unsafe(no_mangle)]
208
+ pub extern "C" fn dama_input_mouse_y() -> f32 {
209
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.mouse_y()).unwrap_or(0.0))).unwrap_or(0.0)
210
+ }
211
+ #[unsafe(no_mangle)]
212
+ pub extern "C" fn dama_input_mouse_button_pressed(button: u32) -> i32 {
213
+ Engine::with(|e| Ok(e.window_state().map(|ws| ws.is_mouse_button_pressed(button)).unwrap_or(false))).unwrap_or(false) as i32
214
+ }
215
+
216
+ /// # Safety
217
+ /// `output_path` must be a valid, non-null, null-terminated C string.
218
+ #[unsafe(no_mangle)]
219
+ pub unsafe extern "C" fn dama_debug_screenshot(output_path: *const c_char) -> i32 {
220
+ let path = CStr::from_ptr(output_path).to_str().map_err(|e| format!("Invalid UTF-8: {e}"));
221
+ match path {
222
+ Ok(path) => ok_or_err(Engine::with(|e| e.screenshot(path)), 0),
223
+ Err(e) => { set_last_error(&e); -1 }
224
+ }
225
+ }
226
+
227
+ // --- Audio ---
228
+
229
+ /// # Safety
230
+ /// `path` must be a valid, non-null, null-terminated C string pointing to an audio file.
231
+ #[unsafe(no_mangle)]
232
+ pub unsafe extern "C" fn dama_audio_load_sound(path: *const c_char) -> u64 {
233
+ let path = CStr::from_ptr(path).to_str().unwrap_or("");
234
+ match crate::audio::load_sound(path) {
235
+ Ok(handle) => handle,
236
+ Err(e) => { set_last_error(&e); 0 }
237
+ }
238
+ }
239
+
240
+ #[unsafe(no_mangle)]
241
+ pub extern "C" fn dama_audio_play_sound(handle: u64, volume: f32, looping: i32) -> i32 {
242
+ ok_or_err(crate::audio::play_sound(handle, volume, looping != 0), 0)
243
+ }
244
+
245
+ #[unsafe(no_mangle)]
246
+ pub extern "C" fn dama_audio_stop_all() -> i32 {
247
+ crate::audio::stop_all();
248
+ 0
249
+ }
250
+
251
+ #[unsafe(no_mangle)]
252
+ pub extern "C" fn dama_audio_unload_sound(handle: u64) -> i32 {
253
+ crate::audio::unload_sound(handle);
254
+ 0
255
+ }
256
+ }
257
+
258
+ #[cfg(not(target_arch = "wasm32"))]
259
+ pub use native_ffi::*;
260
+
261
+ // ===========================================================================
262
+ // Web WASM exports (wasm_bindgen for JavaScript)
263
+ // ===========================================================================
264
+ #[cfg(target_arch = "wasm32")]
265
+ mod web_exports {
266
+ use super::*;
267
+ use wasm_bindgen::prelude::*;
268
+
269
+ #[wasm_bindgen]
270
+ pub fn dama_init(canvas_id: &str, width: u32, height: u32) {
271
+ Engine::init_web(canvas_id, width, height).unwrap();
272
+ }
273
+
274
+ #[wasm_bindgen]
275
+ pub fn dama_shutdown() { let _ = Engine::shutdown(); }
276
+
277
+ #[wasm_bindgen]
278
+ pub fn dama_poll_events() -> bool {
279
+ Engine::pump_events().unwrap_or(false)
280
+ }
281
+
282
+ #[wasm_bindgen]
283
+ pub fn dama_begin_frame() { let _ = Engine::with(|e| e.begin_frame()); }
284
+
285
+ #[wasm_bindgen]
286
+ pub fn dama_end_frame() { let _ = Engine::with(|e| e.end_frame()); }
287
+
288
+ #[wasm_bindgen]
289
+ pub fn dama_delta_time() -> f64 { Engine::with(|e| Ok(e.delta_time())).unwrap_or(0.0) }
290
+
291
+ #[wasm_bindgen]
292
+ pub fn dama_frame_count() -> u64 { Engine::with(|e| Ok(e.frame_count())).unwrap_or(0) }
293
+
294
+ #[wasm_bindgen]
295
+ pub fn dama_clear(r: f32, g: f32, b: f32, a: f32) { let _ = Engine::with(|e| e.renderer().clear(r, g, b, a)); }
296
+
297
+ #[wasm_bindgen]
298
+ pub fn dama_render_vertices(vertex_data: &[f32], vertex_count: u32) {
299
+ let count = vertex_count as usize;
300
+ let _ = Engine::with(|e| { e.renderer().submit_vertices(vertex_data, count); Ok(()) });
301
+ }
302
+
303
+ /// Accept high-level draw commands. Rust decomposes shapes into triangles.
304
+ /// This eliminates geometry decomposition from Ruby/wasm, dramatically
305
+ /// improving web performance.
306
+ #[wasm_bindgen]
307
+ pub fn dama_render_commands(command_data: &[f32], float_count: u32) {
308
+ let count = float_count as usize;
309
+ let _ = Engine::with(|e| { e.renderer().submit_commands(&command_data[..count]); Ok(()) });
310
+ }
311
+
312
+ #[wasm_bindgen]
313
+ pub fn dama_set_texture(handle: u64) {
314
+ let _ = Engine::with(|e| { e.renderer().set_current_texture(handle); Ok(()) });
315
+ }
316
+
317
+ #[wasm_bindgen]
318
+ pub fn dama_load_texture(data: &[u8]) -> u64 {
319
+ Engine::with(|e| e.renderer().load_texture(data)).unwrap_or(0)
320
+ }
321
+
322
+ #[wasm_bindgen]
323
+ pub fn dama_unload_texture(handle: u64) {
324
+ let _ = Engine::with(|e| { e.renderer().unload_texture(handle); Ok(()) });
325
+ }
326
+
327
+ // Shader management.
328
+ #[wasm_bindgen]
329
+ pub fn dama_shader_load(source: &str) -> u64 {
330
+ match Engine::with(|e| e.renderer().load_shader(source)) {
331
+ Ok(handle) => handle,
332
+ Err(_) => 0,
333
+ }
334
+ }
335
+
336
+ #[wasm_bindgen]
337
+ pub fn dama_shader_unload(handle: u64) {
338
+ let _ = Engine::with(|e| { e.renderer().unload_shader(handle); Ok(()) });
339
+ }
340
+
341
+ #[wasm_bindgen]
342
+ pub fn dama_set_shader(handle: u64) {
343
+ let _ = Engine::with(|e| { e.renderer().set_current_shader(handle); Ok(()) });
344
+ }
345
+
346
+ #[wasm_bindgen]
347
+ pub fn dama_render_text(text: &str, x: f32, y: f32, size: f32, r: f32, g: f32, b: f32, a: f32) {
348
+ let _ = Engine::with(|e| { e.renderer().draw_text(text, x, y, size, r, g, b, a, None); Ok(()) });
349
+ }
350
+
351
+ // Input: JS calls these to forward browser events to Rust state.
352
+ #[wasm_bindgen]
353
+ pub fn dama_input_set_key(key_code: u32, pressed: bool) {
354
+ crate::window::InputState::set_key(key_code, pressed);
355
+ }
356
+
357
+ #[wasm_bindgen]
358
+ pub fn dama_input_set_mouse(x: f32, y: f32) {
359
+ crate::window::InputState::set_mouse(x, y);
360
+ }
361
+
362
+ #[wasm_bindgen]
363
+ pub fn dama_input_set_mouse_button(button: u32, pressed: bool) {
364
+ crate::window::InputState::set_mouse_button(button, pressed);
365
+ }
366
+
367
+ #[wasm_bindgen]
368
+ pub fn dama_input_begin_frame() {
369
+ crate::window::InputState::begin_frame();
370
+ }
371
+
372
+ #[wasm_bindgen]
373
+ pub fn dama_key_pressed(key_code: u32) -> bool {
374
+ crate::window::InputState::with(|s| s.is_key_pressed(key_code))
375
+ }
376
+
377
+ #[wasm_bindgen]
378
+ pub fn dama_key_just_pressed(key_code: u32) -> bool {
379
+ crate::window::InputState::with(|s| s.is_key_just_pressed(key_code))
380
+ }
381
+
382
+ #[wasm_bindgen]
383
+ pub fn dama_mouse_x() -> f32 {
384
+ crate::window::InputState::with(|s| s.mouse_x())
385
+ }
386
+
387
+ #[wasm_bindgen]
388
+ pub fn dama_mouse_y() -> f32 {
389
+ crate::window::InputState::with(|s| s.mouse_y())
390
+ }
391
+
392
+ #[wasm_bindgen]
393
+ pub fn dama_mouse_button_pressed(button: u32) -> bool {
394
+ crate::window::InputState::with(|s| s.is_mouse_button_pressed(button))
395
+ }
396
+ }
@@ -0,0 +1,84 @@
1
+ use image::{ImageBuffer, Rgba};
2
+
3
+ /// Capture the contents of a GPU texture to a PNG file.
4
+ ///
5
+ /// This performs a GPU readback: copies the texture into a staging buffer,
6
+ /// maps it to CPU memory, and encodes it as PNG. The `bytes_per_row` is
7
+ /// padded to wgpu's COPY_BYTES_PER_ROW_ALIGNMENT (256 bytes).
8
+ pub fn capture(
9
+ device: &wgpu::Device,
10
+ queue: &wgpu::Queue,
11
+ texture: &wgpu::Texture,
12
+ width: u32,
13
+ height: u32,
14
+ path: &str,
15
+ ) -> Result<(), String> {
16
+ let bytes_per_pixel = 4u32; // Rgba8UnormSrgb
17
+ let unpadded_bytes_per_row = width * bytes_per_pixel;
18
+ let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
19
+ let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
20
+ let buffer_size = (padded_bytes_per_row * height) as u64;
21
+
22
+ let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
23
+ label: Some("screenshot_staging_buffer"),
24
+ size: buffer_size,
25
+ usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
26
+ mapped_at_creation: false,
27
+ });
28
+
29
+ let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
30
+ label: Some("screenshot_encoder"),
31
+ });
32
+
33
+ encoder.copy_texture_to_buffer(
34
+ wgpu::TexelCopyTextureInfo {
35
+ texture,
36
+ mip_level: 0,
37
+ origin: wgpu::Origin3d::ZERO,
38
+ aspect: wgpu::TextureAspect::All,
39
+ },
40
+ wgpu::TexelCopyBufferInfo {
41
+ buffer: &staging_buffer,
42
+ layout: wgpu::TexelCopyBufferLayout {
43
+ offset: 0,
44
+ bytes_per_row: Some(padded_bytes_per_row),
45
+ rows_per_image: Some(height),
46
+ },
47
+ },
48
+ wgpu::Extent3d {
49
+ width,
50
+ height,
51
+ depth_or_array_layers: 1,
52
+ },
53
+ );
54
+
55
+ queue.submit(std::iter::once(encoder.finish()));
56
+
57
+ let buffer_slice = staging_buffer.slice(..);
58
+ let (tx, rx) = std::sync::mpsc::channel();
59
+ buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
60
+ tx.send(result).unwrap();
61
+ });
62
+ let _ = device.poll(wgpu::PollType::wait_indefinitely());
63
+ rx.recv()
64
+ .map_err(|e| format!("Failed to receive map result: {e}"))?
65
+ .map_err(|e| format!("Buffer mapping failed: {e}"))?;
66
+
67
+ let data = buffer_slice.get_mapped_range();
68
+
69
+ // Strip row padding to get contiguous pixel data.
70
+ let mut pixels = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
71
+ for row in 0..height {
72
+ let start = (row * padded_bytes_per_row) as usize;
73
+ let end = start + (unpadded_bytes_per_row) as usize;
74
+ pixels.extend_from_slice(&data[start..end]);
75
+ }
76
+
77
+ drop(data);
78
+ staging_buffer.unmap();
79
+
80
+ let img: ImageBuffer<Rgba<u8>, _> =
81
+ ImageBuffer::from_raw(width, height, pixels).ok_or("Failed to create image buffer")?;
82
+
83
+ img.save(path).map_err(|e| format!("Failed to save PNG: {e}"))
84
+ }