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,563 @@
|
|
|
1
|
+
pub mod screenshot;
|
|
2
|
+
pub mod shape_renderer;
|
|
3
|
+
pub mod text_renderer;
|
|
4
|
+
|
|
5
|
+
use shape_renderer::{ShapeRenderer, Vertex};
|
|
6
|
+
use text_renderer::TextRenderer;
|
|
7
|
+
|
|
8
|
+
/// A batch of vertices grouped by texture and shader handle.
|
|
9
|
+
struct VertexBatch {
|
|
10
|
+
texture_handle: u64,
|
|
11
|
+
shader_handle: u64,
|
|
12
|
+
vertices: Vec<Vertex>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Core wgpu renderer. Manages GPU device, queue, render targets,
|
|
16
|
+
/// and batches vertices by texture for efficient draw calls.
|
|
17
|
+
pub struct Renderer {
|
|
18
|
+
device: wgpu::Device,
|
|
19
|
+
queue: wgpu::Queue,
|
|
20
|
+
width: u32,
|
|
21
|
+
height: u32,
|
|
22
|
+
/// Logical resolution (what the game developer uses for coordinates).
|
|
23
|
+
/// May differ from width/height on HiDPI displays.
|
|
24
|
+
logical_width: u32,
|
|
25
|
+
logical_height: u32,
|
|
26
|
+
render_texture: Option<wgpu::Texture>,
|
|
27
|
+
render_texture_view: Option<wgpu::TextureView>,
|
|
28
|
+
surface_view: Option<wgpu::TextureView>,
|
|
29
|
+
shape_renderer: Option<ShapeRenderer>,
|
|
30
|
+
text_renderer: Option<TextRenderer>,
|
|
31
|
+
pending_batches: Vec<VertexBatch>,
|
|
32
|
+
current_texture: u64,
|
|
33
|
+
current_shader: u64,
|
|
34
|
+
elapsed_time: f32,
|
|
35
|
+
/// Web-only: the wgpu Surface wrapping the HTML canvas.
|
|
36
|
+
#[cfg(target_arch = "wasm32")]
|
|
37
|
+
web_surface: Option<wgpu::Surface<'static>>,
|
|
38
|
+
#[cfg(target_arch = "wasm32")]
|
|
39
|
+
web_surface_texture: Option<wgpu::SurfaceTexture>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl Renderer {
|
|
43
|
+
// Headless mode is native-only (tests).
|
|
44
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
45
|
+
pub fn new_headless(width: u32, height: u32) -> Result<Self, String> {
|
|
46
|
+
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
|
|
47
|
+
|
|
48
|
+
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
49
|
+
power_preference: wgpu::PowerPreference::default(),
|
|
50
|
+
compatible_surface: None,
|
|
51
|
+
force_fallback_adapter: false,
|
|
52
|
+
}))
|
|
53
|
+
.map_err(|e| format!("Failed to find a suitable GPU adapter: {e}"))?;
|
|
54
|
+
|
|
55
|
+
let (device, queue) = pollster::block_on(adapter.request_device(
|
|
56
|
+
&wgpu::DeviceDescriptor {
|
|
57
|
+
label: Some("dama_device"),
|
|
58
|
+
..Default::default()
|
|
59
|
+
},
|
|
60
|
+
))
|
|
61
|
+
.map_err(|e| format!("Failed to create device: {e}"))?;
|
|
62
|
+
|
|
63
|
+
let format = wgpu::TextureFormat::Rgba8Unorm;
|
|
64
|
+
|
|
65
|
+
let render_texture: wgpu::Texture =
|
|
66
|
+
device.create_texture(&wgpu::TextureDescriptor {
|
|
67
|
+
label: Some("headless_render_texture"),
|
|
68
|
+
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
|
69
|
+
mip_level_count: 1,
|
|
70
|
+
sample_count: 1,
|
|
71
|
+
dimension: wgpu::TextureDimension::D2,
|
|
72
|
+
format,
|
|
73
|
+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
|
74
|
+
view_formats: &[],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let render_texture_view =
|
|
78
|
+
render_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
79
|
+
|
|
80
|
+
let shape_renderer = ShapeRenderer::new(&device, &queue, format);
|
|
81
|
+
let text_renderer = TextRenderer::new(&device, &queue, format, width, height);
|
|
82
|
+
|
|
83
|
+
Ok(Self {
|
|
84
|
+
device, queue, width, height,
|
|
85
|
+
logical_width: width, logical_height: height,
|
|
86
|
+
render_texture: Some(render_texture),
|
|
87
|
+
render_texture_view: Some(render_texture_view),
|
|
88
|
+
surface_view: None,
|
|
89
|
+
shape_renderer: Some(shape_renderer),
|
|
90
|
+
text_renderer: Some(text_renderer),
|
|
91
|
+
pending_batches: Vec::new(),
|
|
92
|
+
current_texture: 0,
|
|
93
|
+
current_shader: 0,
|
|
94
|
+
elapsed_time: 0.0,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pub fn new_windowed(device: wgpu::Device, queue: wgpu::Queue, width: u32, height: u32) -> Self {
|
|
99
|
+
Self {
|
|
100
|
+
device, queue, width, height,
|
|
101
|
+
logical_width: width, logical_height: height,
|
|
102
|
+
render_texture: None,
|
|
103
|
+
render_texture_view: None,
|
|
104
|
+
surface_view: None,
|
|
105
|
+
shape_renderer: None,
|
|
106
|
+
text_renderer: None,
|
|
107
|
+
pending_batches: Vec::new(),
|
|
108
|
+
current_texture: 0,
|
|
109
|
+
current_shader: 0,
|
|
110
|
+
elapsed_time: 0.0,
|
|
111
|
+
#[cfg(target_arch = "wasm32")]
|
|
112
|
+
web_surface: None,
|
|
113
|
+
#[cfg(target_arch = "wasm32")]
|
|
114
|
+
web_surface_texture: None,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Store the web surface (called during async init on wasm).
|
|
119
|
+
#[cfg(target_arch = "wasm32")]
|
|
120
|
+
pub fn set_web_surface(&mut self, surface: wgpu::Surface<'static>) {
|
|
121
|
+
self.web_surface = Some(surface);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Acquire the next frame from the web surface.
|
|
125
|
+
#[cfg(target_arch = "wasm32")]
|
|
126
|
+
pub fn acquire_web_surface(&mut self) -> Result<(), String> {
|
|
127
|
+
let surface = self.web_surface.as_ref().ok_or("No web surface")?;
|
|
128
|
+
let texture = surface.get_current_texture()
|
|
129
|
+
.map_err(|e| format!("Failed to get web surface texture: {e}"))?;
|
|
130
|
+
let view = texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
131
|
+
self.surface_view = Some(view);
|
|
132
|
+
self.web_surface_texture = Some(texture);
|
|
133
|
+
Ok(())
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Present the web surface frame.
|
|
137
|
+
#[cfg(target_arch = "wasm32")]
|
|
138
|
+
pub fn present_web_surface(&mut self) {
|
|
139
|
+
if let Some(texture) = self.web_surface_texture.take() {
|
|
140
|
+
texture.present();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub fn set_surface_format(&mut self, format: wgpu::TextureFormat) {
|
|
145
|
+
self.shape_renderer = Some(ShapeRenderer::new(&self.device, &self.queue, format));
|
|
146
|
+
self.text_renderer = Some(TextRenderer::new(
|
|
147
|
+
&self.device, &self.queue, format, self.width, self.height,
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Update physical render dimensions (e.g., after Retina surface creation).
|
|
152
|
+
/// Logical dimensions (used for coordinate mapping) remain unchanged.
|
|
153
|
+
pub fn set_physical_size(&mut self, width: u32, height: u32) {
|
|
154
|
+
self.width = width;
|
|
155
|
+
self.height = height;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Set logical dimensions separately from physical (for HiDPI).
|
|
159
|
+
/// Game coordinates use logical; GPU renders at physical.
|
|
160
|
+
pub fn set_logical_size(&mut self, width: u32, height: u32) {
|
|
161
|
+
self.logical_width = width;
|
|
162
|
+
self.logical_height = height;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn set_surface_view(&mut self, view: Option<wgpu::TextureView>) {
|
|
166
|
+
self.surface_view = view;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn active_view(&self) -> Option<&wgpu::TextureView> {
|
|
170
|
+
self.surface_view.as_ref().or(self.render_texture_view.as_ref())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn begin_frame(&mut self, delta_time: f32) -> Result<(), String> {
|
|
174
|
+
self.pending_batches.clear();
|
|
175
|
+
self.current_texture = 0;
|
|
176
|
+
self.current_shader = 0;
|
|
177
|
+
self.elapsed_time += delta_time;
|
|
178
|
+
|
|
179
|
+
// Update the uniform buffer with current time.
|
|
180
|
+
if let Some(ref sr) = self.shape_renderer {
|
|
181
|
+
sr.update_time(&self.queue, self.elapsed_time);
|
|
182
|
+
}
|
|
183
|
+
Ok(())
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pub fn end_frame(&mut self) -> Result<(), String> {
|
|
187
|
+
if let Some(ref mut tr) = self.text_renderer {
|
|
188
|
+
tr.prepare(&self.device, &self.queue)?;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let view = self.active_view().ok_or("No render target available")?;
|
|
192
|
+
|
|
193
|
+
let mut encoder = self.device.create_command_encoder(
|
|
194
|
+
&wgpu::CommandEncoderDescriptor { label: Some("frame_encoder") },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
199
|
+
label: Some("main_render_pass"),
|
|
200
|
+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
201
|
+
view,
|
|
202
|
+
resolve_target: None,
|
|
203
|
+
depth_slice: None,
|
|
204
|
+
ops: wgpu::Operations {
|
|
205
|
+
load: wgpu::LoadOp::Load,
|
|
206
|
+
store: wgpu::StoreOp::Store,
|
|
207
|
+
},
|
|
208
|
+
})],
|
|
209
|
+
depth_stencil_attachment: None,
|
|
210
|
+
timestamp_writes: None,
|
|
211
|
+
occlusion_query_set: None,
|
|
212
|
+
multiview_mask: None,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Render vertex batches grouped by texture + shader.
|
|
216
|
+
if let Some(ref mut shape_renderer) = self.shape_renderer {
|
|
217
|
+
for batch in &self.pending_batches {
|
|
218
|
+
shape_renderer.render_batch(
|
|
219
|
+
&self.device,
|
|
220
|
+
&mut render_pass,
|
|
221
|
+
&batch.vertices,
|
|
222
|
+
batch.texture_handle,
|
|
223
|
+
batch.shader_handle,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Text overlays everything.
|
|
229
|
+
if let Some(ref self_tr) = self.text_renderer {
|
|
230
|
+
let _ = self_tr.render(&mut render_pass);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
self.queue.submit(std::iter::once(encoder.finish()));
|
|
235
|
+
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
|
|
236
|
+
|
|
237
|
+
if let Some(ref mut tr) = self.text_renderer {
|
|
238
|
+
tr.clear();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Ok(())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pub fn clear(&mut self, r: f32, g: f32, b: f32, a: f32) -> Result<(), String> {
|
|
245
|
+
let view = self.active_view().ok_or("No render target available")?;
|
|
246
|
+
|
|
247
|
+
let mut encoder = self.device.create_command_encoder(
|
|
248
|
+
&wgpu::CommandEncoderDescriptor { label: Some("clear_encoder") },
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
253
|
+
label: Some("clear_pass"),
|
|
254
|
+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
255
|
+
view,
|
|
256
|
+
resolve_target: None,
|
|
257
|
+
depth_slice: None,
|
|
258
|
+
ops: wgpu::Operations {
|
|
259
|
+
load: wgpu::LoadOp::Clear(wgpu::Color {
|
|
260
|
+
r: r as f64, g: g as f64, b: b as f64, a: a as f64,
|
|
261
|
+
}),
|
|
262
|
+
store: wgpu::StoreOp::Store,
|
|
263
|
+
},
|
|
264
|
+
})],
|
|
265
|
+
depth_stencil_attachment: None,
|
|
266
|
+
timestamp_writes: None,
|
|
267
|
+
occlusion_query_set: None,
|
|
268
|
+
multiview_mask: None,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
self.queue.submit(std::iter::once(encoder.finish()));
|
|
273
|
+
Ok(())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Accept pre-decomposed vertices from Ruby.
|
|
277
|
+
/// Each vertex is 8 floats: [x, y, r, g, b, a, u, v] in pixel coordinates.
|
|
278
|
+
pub fn submit_vertices(&mut self, floats: &[f32], vertex_count: usize) {
|
|
279
|
+
// Use logical dimensions for NDC conversion — game coordinates are in logical pixels.
|
|
280
|
+
let w = self.logical_width as f32;
|
|
281
|
+
let h = self.logical_height as f32;
|
|
282
|
+
let tex = self.current_texture;
|
|
283
|
+
let shd = self.current_shader;
|
|
284
|
+
|
|
285
|
+
// Find or create a batch for the current texture + shader.
|
|
286
|
+
let batch = self.pending_batches.iter_mut()
|
|
287
|
+
.rfind(|b| b.texture_handle == tex && b.shader_handle == shd);
|
|
288
|
+
|
|
289
|
+
let batch = match batch {
|
|
290
|
+
Some(b) => b,
|
|
291
|
+
None => {
|
|
292
|
+
self.pending_batches.push(VertexBatch {
|
|
293
|
+
texture_handle: tex,
|
|
294
|
+
shader_handle: shd,
|
|
295
|
+
vertices: Vec::new(),
|
|
296
|
+
});
|
|
297
|
+
self.pending_batches.last_mut().unwrap()
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
for i in 0..vertex_count {
|
|
302
|
+
let base = i * 8;
|
|
303
|
+
let px = floats[base];
|
|
304
|
+
let py = floats[base + 1];
|
|
305
|
+
|
|
306
|
+
batch.vertices.push(Vertex {
|
|
307
|
+
position: [
|
|
308
|
+
(px / w) * 2.0 - 1.0,
|
|
309
|
+
1.0 - (py / h) * 2.0,
|
|
310
|
+
],
|
|
311
|
+
color: [
|
|
312
|
+
floats[base + 2],
|
|
313
|
+
floats[base + 3],
|
|
314
|
+
floats[base + 4],
|
|
315
|
+
floats[base + 5],
|
|
316
|
+
],
|
|
317
|
+
uv: [
|
|
318
|
+
floats[base + 6],
|
|
319
|
+
floats[base + 7],
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Accept high-level draw commands from Ruby (web backend).
|
|
326
|
+
/// Each command starts with a float type tag, followed by shape-specific data.
|
|
327
|
+
/// Rust decomposes shapes into triangles — eliminating trig from Ruby/wasm.
|
|
328
|
+
///
|
|
329
|
+
/// Command format:
|
|
330
|
+
/// 0 = Circle: [0, cx, cy, radius, r, g, b, a, segments] (9 floats)
|
|
331
|
+
/// 1 = Rect: [1, x, y, w, h, r, g, b, a] (9 floats)
|
|
332
|
+
/// 2 = Triangle: [2, x1, y1, x2, y2, x3, y3, r, g, b, a] (11 floats)
|
|
333
|
+
/// 3 = Sprite: [3, handle, x, y, w, h, r, g, b, a, u0, v0, u1, v1] (14 floats)
|
|
334
|
+
/// 4 = SetTexture: [4, handle] (2 floats)
|
|
335
|
+
/// 5 = SetShader: [5, handle] (2 floats)
|
|
336
|
+
pub fn submit_commands(&mut self, commands: &[f32]) {
|
|
337
|
+
const COMMAND_SIZES: [usize; 6] = [9, 9, 11, 14, 2, 2];
|
|
338
|
+
|
|
339
|
+
let mut cursor = 0;
|
|
340
|
+
while cursor < commands.len() {
|
|
341
|
+
let tag = commands[cursor] as usize;
|
|
342
|
+
if tag >= COMMAND_SIZES.len() {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let size = COMMAND_SIZES[tag];
|
|
347
|
+
if cursor + size > commands.len() {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let cmd = &commands[cursor..cursor + size];
|
|
352
|
+
match tag {
|
|
353
|
+
0 => self.decompose_circle(cmd),
|
|
354
|
+
1 => self.decompose_rect(cmd),
|
|
355
|
+
2 => self.decompose_triangle(cmd),
|
|
356
|
+
3 => self.decompose_sprite(cmd),
|
|
357
|
+
4 => self.current_texture = cmd[1] as u64,
|
|
358
|
+
5 => self.current_shader = cmd[1] as u64,
|
|
359
|
+
_ => {}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
cursor += size;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Convert pixel coordinates to Normalized Device Coordinates.
|
|
367
|
+
fn pixel_to_ndc(&self, px: f32, py: f32) -> [f32; 2] {
|
|
368
|
+
let w = self.logical_width as f32;
|
|
369
|
+
let h = self.logical_height as f32;
|
|
370
|
+
[(px / w) * 2.0 - 1.0, 1.0 - (py / h) * 2.0]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Push a single vertex (in pixel coords) to the current texture+shader batch.
|
|
374
|
+
fn push_vertex(&mut self, px: f32, py: f32, r: f32, g: f32, b: f32, a: f32, u: f32, v: f32) {
|
|
375
|
+
let pos = self.pixel_to_ndc(px, py);
|
|
376
|
+
let tex = self.current_texture;
|
|
377
|
+
let shd = self.current_shader;
|
|
378
|
+
|
|
379
|
+
let batch = self.pending_batches.iter_mut()
|
|
380
|
+
.rfind(|b| b.texture_handle == tex && b.shader_handle == shd);
|
|
381
|
+
|
|
382
|
+
let batch = match batch {
|
|
383
|
+
Some(b) => b,
|
|
384
|
+
None => {
|
|
385
|
+
self.pending_batches.push(VertexBatch {
|
|
386
|
+
texture_handle: tex,
|
|
387
|
+
shader_handle: shd,
|
|
388
|
+
vertices: Vec::new(),
|
|
389
|
+
});
|
|
390
|
+
self.pending_batches.last_mut().unwrap()
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
batch.vertices.push(Vertex {
|
|
395
|
+
position: pos,
|
|
396
|
+
color: [r, g, b, a],
|
|
397
|
+
uv: [u, v],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Decompose a circle command into a triangle fan.
|
|
402
|
+
/// cmd: [0, cx, cy, radius, r, g, b, a, segments]
|
|
403
|
+
fn decompose_circle(&mut self, cmd: &[f32]) {
|
|
404
|
+
let cx = cmd[1];
|
|
405
|
+
let cy = cmd[2];
|
|
406
|
+
let radius = cmd[3];
|
|
407
|
+
let r = cmd[4];
|
|
408
|
+
let g = cmd[5];
|
|
409
|
+
let b = cmd[6];
|
|
410
|
+
let a = cmd[7];
|
|
411
|
+
let segments = cmd[8] as u32;
|
|
412
|
+
|
|
413
|
+
let step = std::f32::consts::TAU / segments as f32;
|
|
414
|
+
for i in 0..segments {
|
|
415
|
+
let a1 = step * i as f32;
|
|
416
|
+
let a2 = step * (i + 1) as f32;
|
|
417
|
+
let x1 = cx + radius * a1.cos();
|
|
418
|
+
let y1 = cy + radius * a1.sin();
|
|
419
|
+
let x2 = cx + radius * a2.cos();
|
|
420
|
+
let y2 = cy + radius * a2.sin();
|
|
421
|
+
|
|
422
|
+
self.push_vertex(cx, cy, r, g, b, a, 0.0, 0.0);
|
|
423
|
+
self.push_vertex(x1, y1, r, g, b, a, 0.0, 0.0);
|
|
424
|
+
self.push_vertex(x2, y2, r, g, b, a, 0.0, 0.0);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Decompose a rect command into 2 triangles (6 vertices).
|
|
429
|
+
/// cmd: [1, x, y, w, h, r, g, b, a]
|
|
430
|
+
fn decompose_rect(&mut self, cmd: &[f32]) {
|
|
431
|
+
let x = cmd[1];
|
|
432
|
+
let y = cmd[2];
|
|
433
|
+
let w = cmd[3];
|
|
434
|
+
let h = cmd[4];
|
|
435
|
+
let r = cmd[5];
|
|
436
|
+
let g = cmd[6];
|
|
437
|
+
let b = cmd[7];
|
|
438
|
+
let a = cmd[8];
|
|
439
|
+
|
|
440
|
+
// Triangle 1: top-left, top-right, bottom-left
|
|
441
|
+
self.push_vertex(x, y, r, g, b, a, 0.0, 0.0);
|
|
442
|
+
self.push_vertex(x + w, y, r, g, b, a, 0.0, 0.0);
|
|
443
|
+
self.push_vertex(x, y + h, r, g, b, a, 0.0, 0.0);
|
|
444
|
+
// Triangle 2: top-right, bottom-right, bottom-left
|
|
445
|
+
self.push_vertex(x + w, y, r, g, b, a, 0.0, 0.0);
|
|
446
|
+
self.push_vertex(x + w, y + h, r, g, b, a, 0.0, 0.0);
|
|
447
|
+
self.push_vertex(x, y + h, r, g, b, a, 0.0, 0.0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// Pass through a triangle command as 3 vertices.
|
|
451
|
+
/// cmd: [2, x1, y1, x2, y2, x3, y3, r, g, b, a]
|
|
452
|
+
fn decompose_triangle(&mut self, cmd: &[f32]) {
|
|
453
|
+
let r = cmd[7];
|
|
454
|
+
let g = cmd[8];
|
|
455
|
+
let b = cmd[9];
|
|
456
|
+
let a = cmd[10];
|
|
457
|
+
|
|
458
|
+
self.push_vertex(cmd[1], cmd[2], r, g, b, a, 0.0, 0.0);
|
|
459
|
+
self.push_vertex(cmd[3], cmd[4], r, g, b, a, 0.0, 0.0);
|
|
460
|
+
self.push_vertex(cmd[5], cmd[6], r, g, b, a, 0.0, 0.0);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// Decompose a sprite command into a textured quad (6 vertices).
|
|
464
|
+
/// cmd: [3, handle, x, y, w, h, r, g, b, a, u_min, v_min, u_max, v_max]
|
|
465
|
+
fn decompose_sprite(&mut self, cmd: &[f32]) {
|
|
466
|
+
let handle = cmd[1] as u64;
|
|
467
|
+
let x = cmd[2];
|
|
468
|
+
let y = cmd[3];
|
|
469
|
+
let w = cmd[4];
|
|
470
|
+
let h = cmd[5];
|
|
471
|
+
let r = cmd[6];
|
|
472
|
+
let g = cmd[7];
|
|
473
|
+
let b = cmd[8];
|
|
474
|
+
let a = cmd[9];
|
|
475
|
+
let u_min = cmd[10];
|
|
476
|
+
let v_min = cmd[11];
|
|
477
|
+
let u_max = cmd[12];
|
|
478
|
+
let v_max = cmd[13];
|
|
479
|
+
|
|
480
|
+
// Temporarily switch texture for this sprite.
|
|
481
|
+
let prev_texture = self.current_texture;
|
|
482
|
+
self.current_texture = handle;
|
|
483
|
+
|
|
484
|
+
self.push_vertex(x, y, r, g, b, a, u_min, v_min);
|
|
485
|
+
self.push_vertex(x + w, y, r, g, b, a, u_max, v_min);
|
|
486
|
+
self.push_vertex(x, y + h, r, g, b, a, u_min, v_max);
|
|
487
|
+
self.push_vertex(x + w, y, r, g, b, a, u_max, v_min);
|
|
488
|
+
self.push_vertex(x + w, y + h, r, g, b, a, u_max, v_max);
|
|
489
|
+
self.push_vertex(x, y + h, r, g, b, a, u_min, v_max);
|
|
490
|
+
|
|
491
|
+
// Restore previous texture.
|
|
492
|
+
self.current_texture = prev_texture;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/// Set the current texture for subsequent vertex submissions.
|
|
496
|
+
/// handle=0 means no texture (white pixel default).
|
|
497
|
+
pub fn set_current_texture(&mut self, handle: u64) {
|
|
498
|
+
self.current_texture = handle;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Load a texture from raw image bytes. Returns a handle.
|
|
502
|
+
pub fn load_texture(&mut self, data: &[u8]) -> Result<u64, String> {
|
|
503
|
+
let sr = self.shape_renderer.as_mut()
|
|
504
|
+
.ok_or("Shape renderer not initialized")?;
|
|
505
|
+
sr.load_texture(&self.device, &self.queue, data)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// Unload a previously loaded texture.
|
|
509
|
+
pub fn unload_texture(&mut self, handle: u64) {
|
|
510
|
+
if let Some(ref mut sr) = self.shape_renderer {
|
|
511
|
+
sr.unload_texture(handle);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Set the current shader for subsequent vertex submissions.
|
|
516
|
+
/// handle=0 means default shader.
|
|
517
|
+
pub fn set_current_shader(&mut self, handle: u64) {
|
|
518
|
+
self.current_shader = handle;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/// Load a custom WGSL fragment shader. Returns a handle.
|
|
522
|
+
pub fn load_shader(&mut self, source: &str) -> Result<u64, String> {
|
|
523
|
+
let sr = self.shape_renderer.as_mut()
|
|
524
|
+
.ok_or("Shape renderer not initialized")?;
|
|
525
|
+
Ok(sr.load_shader(source))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/// Unload a previously loaded shader.
|
|
529
|
+
pub fn unload_shader(&mut self, handle: u64) {
|
|
530
|
+
if let Some(ref mut sr) = self.shape_renderer {
|
|
531
|
+
sr.unload_shader(handle);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
pub fn draw_text(
|
|
536
|
+
&mut self, text: &str, x: f32, y: f32, size: f32,
|
|
537
|
+
r: f32, g: f32, b: f32, a: f32,
|
|
538
|
+
font_family: Option<&str>,
|
|
539
|
+
) {
|
|
540
|
+
if let Some(ref mut tr) = self.text_renderer {
|
|
541
|
+
let scale_x = self.width as f32 / self.logical_width as f32;
|
|
542
|
+
let scale_y = self.height as f32 / self.logical_height as f32;
|
|
543
|
+
tr.queue_text(text, x * scale_x, y * scale_y, size * scale_x, r, g, b, a, font_family);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// Load custom font data into the text renderer's font system.
|
|
548
|
+
pub fn load_font(&mut self, data: Vec<u8>) {
|
|
549
|
+
if let Some(ref mut tr) = self.text_renderer {
|
|
550
|
+
tr.load_font(data);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
pub fn screenshot(&self, path: &str) -> Result<(), String> {
|
|
555
|
+
let texture = self.render_texture.as_ref()
|
|
556
|
+
.ok_or("Screenshot only works in headless mode")?;
|
|
557
|
+
screenshot::capture(&self.device, &self.queue, texture, self.width, self.height, path)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
pub fn device(&self) -> &wgpu::Device {
|
|
561
|
+
&self.device
|
|
562
|
+
}
|
|
563
|
+
}
|