dama 0.1.0-x86_64-linux
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/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/native/libdama_native.so +0 -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 +186 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Backend
|
|
3
|
+
# Abstract interface for rendering backends. Defines the contract
|
|
4
|
+
# that all backends (native, web, etc.) must implement.
|
|
5
|
+
# Each method raises NotImplementedError by default.
|
|
6
|
+
class Base
|
|
7
|
+
def initialize_engine(configuration:)
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def shutdown
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def poll_events
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def begin_frame
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def end_frame
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delta_time
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def frame_count
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
|
|
40
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
45
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
|
|
50
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
|
|
55
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def load_font(path:)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
64
|
+
r: color.r, g: color.g, b: color.b, a: color.a)
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_texture(bytes:)
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_texture_file(path:)
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unload_texture(handle:)
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def screenshot(output_path:)
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def key_pressed?(key_code:)
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def key_just_pressed?(key_code:)
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def key_just_released?(key_code:)
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mouse_x
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mouse_y
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def mouse_button_pressed?(button:)
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_sound(path:)
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def play_sound(handle:, volume: 1.0, loop: false)
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def stop_all_sounds
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def unload_sound(handle:)
|
|
121
|
+
raise NotImplementedError
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def load_shader(source:)
|
|
125
|
+
raise NotImplementedError
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def unload_shader(handle:)
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def set_shader(handle:)
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require "ffi"
|
|
2
|
+
|
|
3
|
+
module Dama
|
|
4
|
+
module Backend
|
|
5
|
+
class Native
|
|
6
|
+
# Raw FFI bindings to the Rust cdylib. This is the only place in the
|
|
7
|
+
# Ruby codebase where FFI types appear. All other code interacts
|
|
8
|
+
# through the Backend::Native adapter.
|
|
9
|
+
module FfiBindings
|
|
10
|
+
extend FFI::Library
|
|
11
|
+
|
|
12
|
+
# Platform-specific shared library extension, resolved via hash
|
|
13
|
+
# lookup to avoid conditionals (per project coding guidelines).
|
|
14
|
+
LIBRARY_EXTENSIONS = {
|
|
15
|
+
"darwin" => "dylib",
|
|
16
|
+
"linux" => "so",
|
|
17
|
+
"mingw" => "dll",
|
|
18
|
+
"mswin" => "dll",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Rust omits the "lib" prefix on Windows cdylibs (dama_native.dll
|
|
22
|
+
# instead of libdama_native.dll), so we need platform-aware names.
|
|
23
|
+
LIBRARY_PREFIXES = {
|
|
24
|
+
"darwin" => "lib",
|
|
25
|
+
"linux" => "lib",
|
|
26
|
+
"mingw" => "",
|
|
27
|
+
"mswin" => "",
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def self.library_filename
|
|
31
|
+
platform_key = LIBRARY_EXTENSIONS.keys.detect { |k| RUBY_PLATFORM.include?(k) }
|
|
32
|
+
prefix = LIBRARY_PREFIXES.fetch(platform_key)
|
|
33
|
+
extension = LIBRARY_EXTENSIONS.fetch(platform_key)
|
|
34
|
+
"#{prefix}dama_native.#{extension}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Library resolution order:
|
|
38
|
+
# 1. DAMA_NATIVE_LIB env var — packaged games set this to their bundled copy
|
|
39
|
+
# 2. lib/dama/native/ — pre-compiled platform gems and source gem extconf.rb
|
|
40
|
+
# install the shared library here
|
|
41
|
+
# 3. ext/dama_native/target/release/ — local development with cargo build
|
|
42
|
+
LIBRARY_PATH_RESOLVERS = [
|
|
43
|
+
lambda {
|
|
44
|
+
path = ENV.fetch("DAMA_NATIVE_LIB", nil)
|
|
45
|
+
path if path && File.exist?(path)
|
|
46
|
+
},
|
|
47
|
+
lambda {
|
|
48
|
+
path = File.expand_path("../../native/#{library_filename}", __dir__)
|
|
49
|
+
path if File.exist?(path)
|
|
50
|
+
},
|
|
51
|
+
lambda {
|
|
52
|
+
path = File.expand_path("../../../../ext/dama_native/target/release/#{library_filename}", __dir__)
|
|
53
|
+
path if File.exist?(path)
|
|
54
|
+
},
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def self.library_path
|
|
58
|
+
LIBRARY_PATH_RESOLVERS.each do |resolver|
|
|
59
|
+
path = resolver.call
|
|
60
|
+
return path if path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
raise "dama native library not found. Run `cargo build --release` in ext/dama_native/ " \
|
|
64
|
+
"or install a platform-specific gem."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ffi_lib library_path
|
|
68
|
+
|
|
69
|
+
# --- Lifecycle ---
|
|
70
|
+
attach_function :dama_engine_init_headless, %i[uint32 uint32], :int32
|
|
71
|
+
attach_function :dama_engine_init, %i[uint32 uint32 string], :int32
|
|
72
|
+
attach_function :dama_engine_shutdown, [], :int32
|
|
73
|
+
attach_function :dama_engine_poll_events, [], :int32
|
|
74
|
+
attach_function :dama_engine_begin_frame, [], :int32
|
|
75
|
+
attach_function :dama_engine_end_frame, [], :int32
|
|
76
|
+
attach_function :dama_engine_delta_time, [], :double
|
|
77
|
+
attach_function :dama_engine_frame_count, [], :uint64
|
|
78
|
+
attach_function :dama_engine_last_error, [], :string
|
|
79
|
+
|
|
80
|
+
# --- Rendering ---
|
|
81
|
+
attach_function :dama_render_clear, %i[float float float float], :int32
|
|
82
|
+
attach_function :dama_render_vertices, %i[pointer uint32], :int32
|
|
83
|
+
attach_function :dama_render_set_texture, [:uint64], :int32
|
|
84
|
+
attach_function :dama_render_text,
|
|
85
|
+
%i[string float float float
|
|
86
|
+
float float float float], :int32
|
|
87
|
+
|
|
88
|
+
# --- Assets ---
|
|
89
|
+
attach_function :dama_asset_load_texture, %i[pointer uint32], :uint64
|
|
90
|
+
attach_function :dama_asset_unload_texture, [:uint64], :int32
|
|
91
|
+
|
|
92
|
+
# --- Input ---
|
|
93
|
+
attach_function :dama_input_key_pressed, [:uint32], :int32
|
|
94
|
+
attach_function :dama_input_key_just_pressed, [:uint32], :int32
|
|
95
|
+
attach_function :dama_input_key_just_released, [:uint32], :int32
|
|
96
|
+
attach_function :dama_input_mouse_x, [], :float
|
|
97
|
+
attach_function :dama_input_mouse_y, [], :float
|
|
98
|
+
attach_function :dama_input_mouse_button_pressed, [:uint32], :int32
|
|
99
|
+
|
|
100
|
+
# --- Debug ---
|
|
101
|
+
attach_function :dama_debug_screenshot, [:string], :int32
|
|
102
|
+
|
|
103
|
+
# --- Fonts ---
|
|
104
|
+
attach_function :dama_font_load, [:string], :int32
|
|
105
|
+
attach_function :dama_render_text_with_font,
|
|
106
|
+
%i[string float float float
|
|
107
|
+
float float float float string], :int32
|
|
108
|
+
|
|
109
|
+
# --- Audio ---
|
|
110
|
+
attach_function :dama_audio_load_sound, [:string], :uint64
|
|
111
|
+
attach_function :dama_audio_play_sound, %i[uint64 float int32], :int32
|
|
112
|
+
attach_function :dama_audio_stop_all, [], :int32
|
|
113
|
+
attach_function :dama_audio_unload_sound, [:uint64], :int32
|
|
114
|
+
|
|
115
|
+
# --- Shaders ---
|
|
116
|
+
attach_function :dama_shader_load, [:string], :uint64
|
|
117
|
+
attach_function :dama_shader_unload, [:uint64], :int32
|
|
118
|
+
attach_function :dama_render_set_shader, [:uint64], :int32
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Backend
|
|
3
|
+
# Native backend: calls the Rust cdylib through ruby-ffi.
|
|
4
|
+
# Shapes are decomposed into vertices in Ruby and submitted
|
|
5
|
+
# as a single batch per frame via dama_render_vertices.
|
|
6
|
+
class Native < Base
|
|
7
|
+
HEADLESS_INIT = lambda { |bindings, config|
|
|
8
|
+
bindings.dama_engine_init_headless(config.width, config.height)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# :nocov:
|
|
12
|
+
WINDOWED_INIT = ->(bindings, config) { bindings.dama_engine_init(config.width, config.height, config.title) }
|
|
13
|
+
# :nocov:
|
|
14
|
+
|
|
15
|
+
INIT_STRATEGIES = { true => HEADLESS_INIT, false => WINDOWED_INIT }.freeze
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@bindings = Native::FfiBindings
|
|
19
|
+
@vertex_batch = VertexBatch.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize_engine(configuration:)
|
|
23
|
+
strategy = INIT_STRATEGIES.fetch(configuration.headless)
|
|
24
|
+
result = strategy.call(bindings, configuration)
|
|
25
|
+
check_result(result:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shutdown
|
|
29
|
+
check_result(result: bindings.dama_engine_shutdown)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def poll_events
|
|
33
|
+
result = bindings.dama_engine_poll_events
|
|
34
|
+
result == 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def begin_frame
|
|
38
|
+
check_result(result: bindings.dama_engine_begin_frame)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def end_frame
|
|
42
|
+
# Flush accumulated vertices to the GPU in one FFI call.
|
|
43
|
+
vertex_batch.flush(bindings:)
|
|
44
|
+
check_result(result: bindings.dama_engine_end_frame)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delta_time
|
|
48
|
+
bindings.dama_engine_delta_time
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def frame_count
|
|
52
|
+
bindings.dama_engine_frame_count
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
|
|
56
|
+
check_result(result: bindings.dama_render_clear(r, g, b, a))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
|
|
60
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
61
|
+
vertex_batch.push(Geometry::Triangle.vertices(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
65
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
66
|
+
vertex_batch.push(Geometry::Rect.vertices(x:, y:, w:, h:, r:, g:, b:, a:))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
|
|
70
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
|
|
71
|
+
vertex_batch.push(Geometry::Circle.vertices(cx:, cy:, radius:, r:, g:, b:, a:, segments:))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
|
|
75
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
76
|
+
vertex_batch.flush(bindings:)
|
|
77
|
+
result = if font
|
|
78
|
+
bindings.dama_render_text_with_font(text, x, y, size, r, g, b, a, font)
|
|
79
|
+
else
|
|
80
|
+
bindings.dama_render_text(text, x, y, size, r, g, b, a)
|
|
81
|
+
end
|
|
82
|
+
check_result(result:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_font(path:)
|
|
86
|
+
check_result(result: bindings.dama_font_load(path))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
90
|
+
r: color.r, g: color.g, b: color.b, a: color.a)
|
|
91
|
+
# Flush any untextured vertices, switch texture, push sprite, flush, reset.
|
|
92
|
+
vertex_batch.flush(bindings:)
|
|
93
|
+
check_result(result: bindings.dama_render_set_texture(texture_handle))
|
|
94
|
+
vertex_batch.push(Geometry::Sprite.vertices(x:, y:, w:, h:, r:, g:, b:, a:))
|
|
95
|
+
vertex_batch.flush(bindings:)
|
|
96
|
+
check_result(result: bindings.dama_render_set_texture(0))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def load_texture(bytes:)
|
|
100
|
+
ptr = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
101
|
+
ptr.put_bytes(0, bytes)
|
|
102
|
+
handle = bindings.dama_asset_load_texture(ptr, bytes.bytesize)
|
|
103
|
+
raise "Failed to load texture" if handle.zero?
|
|
104
|
+
|
|
105
|
+
handle
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_texture_file(path:)
|
|
109
|
+
load_texture(bytes: File.binread(path))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unload_texture(handle:)
|
|
113
|
+
check_result(result: bindings.dama_asset_unload_texture(handle))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def screenshot(output_path:)
|
|
117
|
+
check_result(result: bindings.dama_debug_screenshot(output_path))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def key_pressed?(key_code:)
|
|
121
|
+
bindings.dama_input_key_pressed(key_code) == 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def key_just_pressed?(key_code:)
|
|
125
|
+
bindings.dama_input_key_just_pressed(key_code) == 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def key_just_released?(key_code:)
|
|
129
|
+
bindings.dama_input_key_just_released(key_code) == 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def mouse_x
|
|
133
|
+
bindings.dama_input_mouse_x
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mouse_y
|
|
137
|
+
bindings.dama_input_mouse_y
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mouse_button_pressed?(button:)
|
|
141
|
+
bindings.dama_input_mouse_button_pressed(button) == 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def load_sound(path:)
|
|
145
|
+
handle = bindings.dama_audio_load_sound(path)
|
|
146
|
+
raise "Failed to load sound: #{bindings.dama_engine_last_error}" if handle.zero?
|
|
147
|
+
|
|
148
|
+
handle
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def play_sound(handle:, volume: 1.0, loop: false)
|
|
152
|
+
looping = loop ? 1 : 0
|
|
153
|
+
check_result(result: bindings.dama_audio_play_sound(handle, volume, looping))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def stop_all_sounds
|
|
157
|
+
check_result(result: bindings.dama_audio_stop_all)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def unload_sound(handle:)
|
|
161
|
+
check_result(result: bindings.dama_audio_unload_sound(handle))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def load_shader(source:)
|
|
165
|
+
bindings.dama_shader_load(source)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def unload_shader(handle:)
|
|
169
|
+
check_result(result: bindings.dama_shader_unload(handle))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def set_shader(handle:)
|
|
173
|
+
# Flush pending vertices before changing shader to ensure
|
|
174
|
+
# they render with the current shader, not the new one.
|
|
175
|
+
vertex_batch.flush(bindings:)
|
|
176
|
+
check_result(result: bindings.dama_render_set_shader(handle))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
attr_reader :bindings, :vertex_batch
|
|
182
|
+
|
|
183
|
+
def check_result(result:)
|
|
184
|
+
return if result >= 0
|
|
185
|
+
|
|
186
|
+
error_msg = bindings.dama_engine_last_error
|
|
187
|
+
raise error_msg || "Unknown native engine error"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Backend
|
|
3
|
+
# Web backend: runs in ruby.wasm, sends high-level draw commands
|
|
4
|
+
# to the Rust wgpu wasm renderer via JavaScript bridge.
|
|
5
|
+
#
|
|
6
|
+
# Instead of decomposing shapes into triangles in Ruby (expensive in wasm),
|
|
7
|
+
# we send compact commands (9-14 floats each) and let Rust decompose them
|
|
8
|
+
# at native speed via dama_render_commands.
|
|
9
|
+
class Web < Base
|
|
10
|
+
def initialize
|
|
11
|
+
@frame_count = 0
|
|
12
|
+
@command_buffer = CommandBuffer.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize_engine(configuration:)
|
|
16
|
+
# On web, the engine is already initialized by index.html.
|
|
17
|
+
# Only call dama_init if not yet ready (avoids double-init replacing shaders).
|
|
18
|
+
w = configuration.width
|
|
19
|
+
h = configuration.height
|
|
20
|
+
::JS.eval("if (!window.__damaReady) { window.damaWgpu.dama_init('game', #{w}, #{h}); }")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shutdown; end
|
|
24
|
+
def poll_events = false
|
|
25
|
+
|
|
26
|
+
def begin_frame
|
|
27
|
+
command_buffer.clear
|
|
28
|
+
js_renderer.call(:dama_begin_frame)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def end_frame
|
|
32
|
+
flush_commands
|
|
33
|
+
js_renderer.call(:dama_end_frame)
|
|
34
|
+
@frame_count += 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delta_time
|
|
38
|
+
js_time[:delta].to_f
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :frame_count
|
|
42
|
+
|
|
43
|
+
def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
|
|
44
|
+
js_renderer.call(:dama_clear, r, g, b, a)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
|
|
48
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
49
|
+
command_buffer.push_triangle(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
53
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
54
|
+
command_buffer.push_rect(x:, y:, w:, h:, r:, g:, b:, a:)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
|
|
58
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
|
|
59
|
+
command_buffer.push_circle(cx:, cy:, radius:, r:, g:, b:, a:, segments:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
|
|
63
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
64
|
+
flush_commands
|
|
65
|
+
js_renderer.call(:dama_render_text, text, x, y, size, r, g, b, a)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
69
|
+
r: color.r, g: color.g, b: color.b, a: color.a)
|
|
70
|
+
command_buffer.push_sprite(
|
|
71
|
+
texture_handle:, x:, y:, w:, h:, r:, g:, b:, a:,
|
|
72
|
+
u_min: 0.0, v_min: 0.0, u_max: 1.0, v_max: 1.0
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def screenshot(output_path:); end
|
|
77
|
+
|
|
78
|
+
def key_pressed?(key_code:)
|
|
79
|
+
js_renderer.call(:dama_key_pressed, key_code).to_s == "true"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def key_just_pressed?(key_code:)
|
|
83
|
+
js_renderer.call(:dama_key_just_pressed, key_code).to_s == "true"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def key_just_released?(key_code:)
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def mouse_x
|
|
91
|
+
js_renderer.call(:dama_mouse_x).to_f
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def mouse_y
|
|
95
|
+
js_renderer.call(:dama_mouse_y).to_f
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def mouse_button_pressed?(button:)
|
|
99
|
+
::JS.eval("return !!window.damaMouseButtons[#{button}]").to_s == "true"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def load_texture(bytes:)
|
|
103
|
+
b64 = [bytes].pack("m0")
|
|
104
|
+
js_array = ::JS.eval("return Uint8Array.from(atob('#{b64}'), c => c.charCodeAt(0))")
|
|
105
|
+
from_bigint(js_renderer.call(:dama_load_texture, js_array))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_texture_file(path:)
|
|
109
|
+
load_texture(bytes: File.binread(path))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unload_texture(handle:)
|
|
113
|
+
js_renderer.call(:dama_unload_texture, handle)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_sound(path:)
|
|
117
|
+
@next_sound_handle ||= 0
|
|
118
|
+
@next_sound_handle += 1
|
|
119
|
+
|
|
120
|
+
data = File.binread(path)
|
|
121
|
+
b64 = [data].pack("m0")
|
|
122
|
+
::JS.eval("window.damaSounds = window.damaSounds || {}; " \
|
|
123
|
+
"window.damaSounds[#{@next_sound_handle}] = 'data:audio/wav;base64,#{b64}'")
|
|
124
|
+
@next_sound_handle
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def play_sound(handle:, volume: 1.0, loop: false)
|
|
128
|
+
loop_js = loop ? "a.loop = true;" : ""
|
|
129
|
+
::JS.eval("(() => { const a = new Audio(window.damaSounds[#{handle}]); " \
|
|
130
|
+
"a.volume = #{volume}; #{loop_js} a.play().catch(() => {}); })()")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def stop_all_sounds
|
|
134
|
+
::JS.eval("document.querySelectorAll('audio').forEach(a => { a.pause(); a.currentTime = 0; })")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def unload_sound(handle:)
|
|
138
|
+
::JS.eval("delete window.damaSounds[#{handle}]")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def load_font(path:); end
|
|
142
|
+
|
|
143
|
+
def load_shader(source:)
|
|
144
|
+
# Pass shader source via JS template literal to avoid ruby.wasm
|
|
145
|
+
# JsValue.call data corruption. Escape backticks and backslashes.
|
|
146
|
+
escaped = source.gsub("\\", "\\\\\\\\").gsub("`", "\\`")
|
|
147
|
+
result = ::JS.eval("return String(window.damaWgpu.dama_shader_load(`#{escaped}`))")
|
|
148
|
+
result.to_s.to_i
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unload_shader(handle:)
|
|
152
|
+
js_renderer.call(:dama_shader_unload, to_bigint(handle))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def set_shader(handle:)
|
|
156
|
+
# Merge shader switch INTO the next flush — don't send it separately.
|
|
157
|
+
# This avoids the ruby.wasm state persistence issue between JS.eval calls.
|
|
158
|
+
command_buffer.push_set_shader(shader_handle: handle)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
attr_reader :command_buffer
|
|
164
|
+
|
|
165
|
+
def js_renderer
|
|
166
|
+
::JS.global[:damaWgpu]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def js_time
|
|
170
|
+
::JS.global[:damaTime]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# wasm-bindgen maps Rust u64 to JS BigInt.
|
|
174
|
+
# Ruby integers must be converted before passing to wasm.
|
|
175
|
+
def to_bigint(value)
|
|
176
|
+
::JS.eval("return BigInt(#{value})")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Convert a JS BigInt (from wasm u64 return) to Ruby integer.
|
|
180
|
+
# Uses BigInt.toString() → Ruby String#to_i for reliable conversion.
|
|
181
|
+
def from_bigint(js_bigint)
|
|
182
|
+
js_bigint.call(:toString).to_s.to_i
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Flush accumulated commands to Rust wasm via a single JS.eval call.
|
|
186
|
+
# We pass the data as a JSON array string and construct the Float32Array
|
|
187
|
+
# entirely in JS, because ruby.wasm's JsValue.call doesn't reliably pass
|
|
188
|
+
# typed arrays to wasm-bindgen functions.
|
|
189
|
+
def flush_commands
|
|
190
|
+
return if command_buffer.empty?
|
|
191
|
+
|
|
192
|
+
floats = command_buffer.to_a
|
|
193
|
+
json = floats.map { |f| f.to_f.to_s }.join(",")
|
|
194
|
+
::JS.eval("window.damaWgpu.dama_render_commands(new Float32Array([#{json}]), #{floats.length})")
|
|
195
|
+
command_buffer.clear
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
data/lib/dama/backend.rb
ADDED