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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +227 -0
- data/dama-logo.svg +91 -0
- data/exe/dama +4 -0
- data/ext/dama_native/.cargo/config.toml +3 -0
- data/ext/dama_native/Cargo.lock +3575 -0
- data/ext/dama_native/Cargo.toml +39 -0
- data/ext/dama_native/extconf.rb +72 -0
- data/ext/dama_native/src/audio.rs +134 -0
- data/ext/dama_native/src/engine.rs +339 -0
- data/ext/dama_native/src/lib.rs +396 -0
- data/ext/dama_native/src/renderer/screenshot.rs +84 -0
- data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
- data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
- data/ext/dama_native/src/renderer.rs +563 -0
- data/ext/dama_native/src/window.rs +255 -0
- data/lib/dama/animation.rb +66 -0
- data/lib/dama/asset_cache.rb +56 -0
- data/lib/dama/audio.rb +47 -0
- data/lib/dama/auto_loader.rb +54 -0
- data/lib/dama/backend/base.rb +137 -0
- data/lib/dama/backend/native/ffi_bindings.rb +122 -0
- data/lib/dama/backend/native.rb +191 -0
- data/lib/dama/backend/web.rb +199 -0
- data/lib/dama/backend.rb +13 -0
- data/lib/dama/camera.rb +68 -0
- data/lib/dama/cli/new_project.rb +112 -0
- data/lib/dama/cli/release.rb +45 -0
- data/lib/dama/cli.rb +22 -0
- data/lib/dama/colors.rb +30 -0
- data/lib/dama/command_buffer.rb +83 -0
- data/lib/dama/component/attribute_definition.rb +13 -0
- data/lib/dama/component/attribute_set.rb +32 -0
- data/lib/dama/component.rb +28 -0
- data/lib/dama/configuration.rb +18 -0
- data/lib/dama/debug/frame_controller.rb +35 -0
- data/lib/dama/debug/screenshot_tool.rb +19 -0
- data/lib/dama/debug.rb +4 -0
- data/lib/dama/event_bus.rb +47 -0
- data/lib/dama/game/builder.rb +31 -0
- data/lib/dama/game/loop.rb +44 -0
- data/lib/dama/game.rb +88 -0
- data/lib/dama/geometry/circle.rb +28 -0
- data/lib/dama/geometry/rect.rb +16 -0
- data/lib/dama/geometry/sprite.rb +18 -0
- data/lib/dama/geometry/triangle.rb +13 -0
- data/lib/dama/geometry.rb +4 -0
- data/lib/dama/input/keyboard_state.rb +44 -0
- data/lib/dama/input/mouse_state.rb +45 -0
- data/lib/dama/input.rb +38 -0
- data/lib/dama/keys.rb +67 -0
- data/lib/dama/node/component_slot.rb +18 -0
- data/lib/dama/node/draw_context.rb +96 -0
- data/lib/dama/node.rb +139 -0
- data/lib/dama/physics/body.rb +57 -0
- data/lib/dama/physics/collider.rb +152 -0
- data/lib/dama/physics/collision.rb +15 -0
- data/lib/dama/physics/world.rb +125 -0
- data/lib/dama/physics.rb +4 -0
- data/lib/dama/registry/class_resolver.rb +48 -0
- data/lib/dama/registry.rb +21 -0
- data/lib/dama/release/archiver.rb +100 -0
- data/lib/dama/release/defaults/icon.icns +0 -0
- data/lib/dama/release/defaults/icon.ico +0 -0
- data/lib/dama/release/defaults/icon.png +0 -0
- data/lib/dama/release/dylib_relinker.rb +95 -0
- data/lib/dama/release/game_file_copier.rb +35 -0
- data/lib/dama/release/game_metadata.rb +61 -0
- data/lib/dama/release/icon_provider.rb +36 -0
- data/lib/dama/release/native_builder.rb +44 -0
- data/lib/dama/release/packager/linux.rb +62 -0
- data/lib/dama/release/packager/macos.rb +99 -0
- data/lib/dama/release/packager/web.rb +32 -0
- data/lib/dama/release/packager/windows.rb +61 -0
- data/lib/dama/release/packager.rb +9 -0
- data/lib/dama/release/platform_detector.rb +23 -0
- data/lib/dama/release/ruby_bundler.rb +163 -0
- data/lib/dama/release/stdlib_trimmer.rb +133 -0
- data/lib/dama/release/template_renderer.rb +40 -0
- data/lib/dama/release/templates/info_plist.xml.erb +19 -0
- data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
- data/lib/dama/release.rb +7 -0
- data/lib/dama/scene/composer.rb +65 -0
- data/lib/dama/scene.rb +233 -0
- data/lib/dama/scene_graph/class_index.rb +26 -0
- data/lib/dama/scene_graph/group_node.rb +27 -0
- data/lib/dama/scene_graph/instance_node.rb +30 -0
- data/lib/dama/scene_graph/path_selector.rb +25 -0
- data/lib/dama/scene_graph/query.rb +34 -0
- data/lib/dama/scene_graph/tag_index.rb +26 -0
- data/lib/dama/scene_graph/tree.rb +65 -0
- data/lib/dama/scene_graph.rb +4 -0
- data/lib/dama/sprite_sheet.rb +36 -0
- data/lib/dama/tween/easing.rb +31 -0
- data/lib/dama/tween/lerp.rb +35 -0
- data/lib/dama/tween/manager.rb +28 -0
- data/lib/dama/tween.rb +4 -0
- data/lib/dama/version.rb +3 -0
- data/lib/dama/vertex_batch.rb +35 -0
- data/lib/dama/web/entry.rb +79 -0
- data/lib/dama/web/static/index.html +142 -0
- data/lib/dama/web_builder.rb +232 -0
- data/lib/dama.rb +42 -0
- 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
|
+
}
|