rgame 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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +406 -0
  4. data/docs/api/README.md +167 -0
  5. data/docs/api/app.md +192 -0
  6. data/docs/api/assets.md +426 -0
  7. data/docs/api/audio.md +208 -0
  8. data/docs/api/components.md +321 -0
  9. data/docs/api/drawing.md +330 -0
  10. data/docs/api/game.md +99 -0
  11. data/docs/api/images.md +118 -0
  12. data/docs/api/input.md +179 -0
  13. data/docs/api/internals.md +110 -0
  14. data/docs/api/scene_graph.md +159 -0
  15. data/docs/api/signals.md +142 -0
  16. data/docs/api/systems.md +98 -0
  17. data/docs/api/text.md +116 -0
  18. data/docs/api/toolbox.md +240 -0
  19. data/docs/api/values.md +101 -0
  20. data/ext/README.md +225 -0
  21. data/ext/rgame_core/app/app.c +721 -0
  22. data/ext/rgame_core/app/app_gl.h +64 -0
  23. data/ext/rgame_core/app/frame_loop.c +42 -0
  24. data/ext/rgame_core/app/frame_loop.h +54 -0
  25. data/ext/rgame_core/audio/audio.c +466 -0
  26. data/ext/rgame_core/audio/audio_internal.h +45 -0
  27. data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
  28. data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
  29. data/ext/rgame_core/example.rb +188 -0
  30. data/ext/rgame_core/extconf.rb +167 -0
  31. data/ext/rgame_core/graphics/backend.c +52 -0
  32. data/ext/rgame_core/graphics/backend.h +64 -0
  33. data/ext/rgame_core/graphics/canvas.c +247 -0
  34. data/ext/rgame_core/graphics/canvas.h +143 -0
  35. data/ext/rgame_core/graphics/clip.c +87 -0
  36. data/ext/rgame_core/graphics/clip.h +89 -0
  37. data/ext/rgame_core/graphics/draw_queue.c +216 -0
  38. data/ext/rgame_core/graphics/draw_queue.h +174 -0
  39. data/ext/rgame_core/graphics/gl_backend.c +122 -0
  40. data/ext/rgame_core/graphics/gl_backend.h +43 -0
  41. data/ext/rgame_core/graphics/image.c +304 -0
  42. data/ext/rgame_core/graphics/image_internal.h +30 -0
  43. data/ext/rgame_core/graphics/primitives.c +189 -0
  44. data/ext/rgame_core/graphics/primitives.h +111 -0
  45. data/ext/rgame_core/graphics/recording.c +119 -0
  46. data/ext/rgame_core/graphics/recording.h +88 -0
  47. data/ext/rgame_core/graphics/texture.c +181 -0
  48. data/ext/rgame_core/graphics/texture.h +165 -0
  49. data/ext/rgame_core/graphics/transform.c +128 -0
  50. data/ext/rgame_core/graphics/transform.h +106 -0
  51. data/ext/rgame_core/include/rgame/core.h +577 -0
  52. data/ext/rgame_core/input/device_slots.c +103 -0
  53. data/ext/rgame_core/input/device_slots.h +93 -0
  54. data/ext/rgame_core/input/gamepad.c +145 -0
  55. data/ext/rgame_core/input/gamepad.h +63 -0
  56. data/ext/rgame_core/input/input.c +109 -0
  57. data/ext/rgame_core/input/input.h +99 -0
  58. data/ext/rgame_core/ruby/audio_ext.c +321 -0
  59. data/ext/rgame_core/ruby/core_ext.c +513 -0
  60. data/ext/rgame_core/ruby/core_ext.h +51 -0
  61. data/ext/rgame_core/ruby/font_ext.c +168 -0
  62. data/ext/rgame_core/ruby/image_ext.c +230 -0
  63. data/ext/rgame_core/ruby/recording_ext.c +186 -0
  64. data/ext/rgame_core/ruby/renderer_ext.c +376 -0
  65. data/ext/rgame_core/text/atlas.c +59 -0
  66. data/ext/rgame_core/text/atlas.h +85 -0
  67. data/ext/rgame_core/text/font.c +281 -0
  68. data/ext/rgame_core/text/font.h +139 -0
  69. data/ext/rgame_core/text/font_atlas.c +385 -0
  70. data/ext/rgame_core/text/font_internal.h +47 -0
  71. data/ext/rgame_core/text/glyph_cache.c +142 -0
  72. data/ext/rgame_core/text/glyph_cache.h +89 -0
  73. data/ext/rgame_core/vendor/README.md +159 -0
  74. data/ext/rgame_core/vendor/miniaudio.h +95864 -0
  75. data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
  76. data/ext/rgame_core/vendor/stb_image.h +7988 -0
  77. data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
  78. data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
  79. data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
  80. data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
  81. data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
  82. data/ext/rgame_util/color.c +19 -0
  83. data/ext/rgame_util/color.h +60 -0
  84. data/ext/rgame_util/color_ext.c +156 -0
  85. data/ext/rgame_util/extconf.rb +27 -0
  86. data/ext/rgame_util/tensor.c +186 -0
  87. data/ext/rgame_util/util_ext.c +27 -0
  88. data/ext/rgame_util/util_ext.h +16 -0
  89. data/lib/rgame/boot.rb +13 -0
  90. data/lib/rgame/core/app.rb +82 -0
  91. data/lib/rgame/core/asset_manager.rb +224 -0
  92. data/lib/rgame/core/audio.rb +124 -0
  93. data/lib/rgame/core/font.rb +49 -0
  94. data/lib/rgame/core/gamepad.rb +55 -0
  95. data/lib/rgame/core/image.rb +55 -0
  96. data/lib/rgame/core/input.rb +77 -0
  97. data/lib/rgame/core/nine_slice.rb +163 -0
  98. data/lib/rgame/core/recording.rb +52 -0
  99. data/lib/rgame/core/renderer.rb +363 -0
  100. data/lib/rgame/core/sprite_sheet.rb +108 -0
  101. data/lib/rgame/core/tile_map_renderer.rb +160 -0
  102. data/lib/rgame/core/ui_atlas.rb +86 -0
  103. data/lib/rgame/core.rb +24 -0
  104. data/lib/rgame/engine/actor.rb +53 -0
  105. data/lib/rgame/engine/animation_set.rb +49 -0
  106. data/lib/rgame/engine/animator.rb +44 -0
  107. data/lib/rgame/engine/audio_bus.rb +24 -0
  108. data/lib/rgame/engine/audio_director.rb +29 -0
  109. data/lib/rgame/engine/body.rb +49 -0
  110. data/lib/rgame/engine/cached_label.rb +33 -0
  111. data/lib/rgame/engine/camera.rb +33 -0
  112. data/lib/rgame/engine/camera_view.rb +28 -0
  113. data/lib/rgame/engine/circle_collider.rb +32 -0
  114. data/lib/rgame/engine/collision_box.rb +34 -0
  115. data/lib/rgame/engine/collision_system.rb +44 -0
  116. data/lib/rgame/engine/component.rb +30 -0
  117. data/lib/rgame/engine/components/action_trigger.rb +41 -0
  118. data/lib/rgame/engine/components/animated_sprite.rb +63 -0
  119. data/lib/rgame/engine/components/character_body.rb +70 -0
  120. data/lib/rgame/engine/components/circle_collider.rb +44 -0
  121. data/lib/rgame/engine/components/collision_world.rb +103 -0
  122. data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
  123. data/lib/rgame/engine/components/path_follow.rb +84 -0
  124. data/lib/rgame/engine/components/player_controller.rb +24 -0
  125. data/lib/rgame/engine/components/pool.rb +53 -0
  126. data/lib/rgame/engine/components/screen_wrap.rb +27 -0
  127. data/lib/rgame/engine/components/sprite.rb +31 -0
  128. data/lib/rgame/engine/components/targeting.rb +54 -0
  129. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  130. data/lib/rgame/engine/components/tile_world.rb +68 -0
  131. data/lib/rgame/engine/components/timer.rb +75 -0
  132. data/lib/rgame/engine/components/velocity.rb +27 -0
  133. data/lib/rgame/engine/components/wander_controller.rb +60 -0
  134. data/lib/rgame/engine/debug_overlay.rb +106 -0
  135. data/lib/rgame/engine/i18n.rb +97 -0
  136. data/lib/rgame/engine/input/action_mapper.rb +46 -0
  137. data/lib/rgame/engine/input/actions.rb +41 -0
  138. data/lib/rgame/engine/input/player_controller.rb +14 -0
  139. data/lib/rgame/engine/matrix.rb +32 -0
  140. data/lib/rgame/engine/node2d.rb +271 -0
  141. data/lib/rgame/engine/path.rb +78 -0
  142. data/lib/rgame/engine/pool.rb +51 -0
  143. data/lib/rgame/engine/resettable.rb +67 -0
  144. data/lib/rgame/engine/scene/scene_stack.rb +65 -0
  145. data/lib/rgame/engine/signal.rb +75 -0
  146. data/lib/rgame/engine/spatial_hash.rb +71 -0
  147. data/lib/rgame/engine/tile_collision.rb +78 -0
  148. data/lib/rgame/engine/tile_map.rb +149 -0
  149. data/lib/rgame/engine/tileset.rb +101 -0
  150. data/lib/rgame/engine/timer.rb +51 -0
  151. data/lib/rgame/engine.rb +68 -0
  152. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  153. data/lib/rgame/fonts/OFL.txt +102 -0
  154. data/lib/rgame/game.rb +129 -0
  155. data/lib/rgame/util/color.rb +27 -0
  156. data/lib/rgame/util/controls.rb +107 -0
  157. data/lib/rgame/util/tensor.rb +12 -0
  158. data/lib/rgame/util.rb +8 -0
  159. data/lib/rgame/version.rb +12 -0
  160. data/lib/rgame.rb +20 -0
  161. metadata +215 -0
@@ -0,0 +1,282 @@
1
+ /*
2
+ * vorbis_decoder.c — a miniaudio decoding backend over stb_vorbis.
3
+ * See vorbis_decoder.h for why it exists and why it has two entry points.
4
+ */
5
+
6
+ #include "audio/vorbis_decoder.h"
7
+
8
+ /* Declarations only. The code lives in stb_vorbis_impl.c, which is the one
9
+ * translation unit compiled without the project's warning flags; including it
10
+ * whole here would both duplicate every symbol and drag third-party code
11
+ * through -Wall -Wextra. */
12
+ #define STB_VORBIS_HEADER_ONLY
13
+ #include "vendor/stb_vorbis.c"
14
+
15
+ /*
16
+ * One open ogg stream, as miniaudio sees it.
17
+ *
18
+ * `ma_data_source_base` must come first: miniaudio casts the pointer it is
19
+ * handed straight to its own base type, so anything before it would be read as
20
+ * the base and the first call would go somewhere unfortunate.
21
+ */
22
+ typedef struct {
23
+ ma_data_source_base base;
24
+ stb_vorbis *vorbis;
25
+ ma_uint32 channels;
26
+ ma_uint32 sample_rate;
27
+ /* The compressed file, when it was opened from memory. stb_vorbis keeps
28
+ * reading from this for the life of the stream, so it is freed in uninit
29
+ * and not before. NULL when opened by filename. */
30
+ void *compressed;
31
+ } rgame_vorbis_stream;
32
+
33
+ /* ------------------------------------------------------------------------- *
34
+ * The data source: reading, seeking, and answering questions about the stream
35
+ * ------------------------------------------------------------------------- */
36
+
37
+ static ma_result vorbis_read(ma_data_source *source, void *out, ma_uint64 frames_wanted,
38
+ ma_uint64 *frames_read) {
39
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
40
+ float *destination = (float *)out;
41
+ ma_uint64 filled = 0;
42
+
43
+ /*
44
+ * Keep going until the buffer is full or the file really has run out.
45
+ *
46
+ * One call is not enough: stb_vorbis decodes a packet at a time and
47
+ * routinely hands back fewer frames than were asked for, in the middle of a
48
+ * perfectly healthy file. Callers read a short read as the end — miniaudio's
49
+ * streaming path does exactly that, marking the stream finished when a page
50
+ * comes back under-filled — so passing stb's partial counts straight up
51
+ * makes a streamed song stop after its first packet-run and never loop.
52
+ * That was a real bug here, and it looked like "looping does not work".
53
+ *
54
+ * stb counts in *samples*, one per channel per frame, while miniaudio counts
55
+ * in frames; the multiply below is that conversion, and getting it wrong
56
+ * plays everything at the wrong speed.
57
+ */
58
+ while (filled < frames_wanted) {
59
+ ma_uint64 remaining = frames_wanted - filled;
60
+ int got = stb_vorbis_get_samples_float_interleaved(
61
+ stream->vorbis, (int)stream->channels, destination + (filled * stream->channels),
62
+ (int)(remaining * stream->channels));
63
+
64
+ if (got <= 0) {
65
+ break; /* genuinely out of audio */
66
+ }
67
+ filled += (ma_uint64)got;
68
+ }
69
+
70
+ ma_uint64 got = filled;
71
+ *frames_read = got;
72
+
73
+ /*
74
+ * MA_AT_END rather than an error: running out is how a sound ends.
75
+ *
76
+ * Both halves of this are unobservable through miniaudio, and mutating
77
+ * either survives the suite — measured. miniaudio decides end-of-stream
78
+ * from `frames_read`, so a source that always claimed MA_SUCCESS still
79
+ * makes the decoder report MA_AT_END, and one that always claimed
80
+ * MA_AT_END is ignored while frames keep arriving. It says the true thing
81
+ * anyway: the contract is what a data source is supposed to report, and
82
+ * the next thing to read this code should not have to rediscover that
83
+ * miniaudio happens not to look.
84
+ */
85
+ return got > 0 ? MA_SUCCESS : MA_AT_END;
86
+ }
87
+
88
+ static ma_result vorbis_seek(ma_data_source *source, ma_uint64 frame) {
89
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
90
+ return stb_vorbis_seek(stream->vorbis, (unsigned int)frame) ? MA_SUCCESS : MA_ERROR;
91
+ }
92
+
93
+ static ma_result vorbis_data_format(ma_data_source *source, ma_format *format,
94
+ ma_uint32 *channels, ma_uint32 *sample_rate,
95
+ ma_channel *channel_map, size_t channel_map_capacity) {
96
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
97
+
98
+ if (format) {
99
+ /* Floats, because that is what stb_vorbis hands back and what
100
+ * miniaudio's graph works in — no conversion in the middle. */
101
+ *format = ma_format_f32;
102
+ }
103
+ if (channels) {
104
+ *channels = stream->channels;
105
+ }
106
+ if (sample_rate) {
107
+ *sample_rate = stream->sample_rate;
108
+ }
109
+ if (channel_map) {
110
+ ma_channel_map_init_standard(ma_standard_channel_map_default, channel_map,
111
+ channel_map_capacity, stream->channels);
112
+ }
113
+ return MA_SUCCESS;
114
+ }
115
+
116
+ static ma_result vorbis_cursor(ma_data_source *source, ma_uint64 *cursor) {
117
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
118
+ *cursor = (ma_uint64)stb_vorbis_get_sample_offset(stream->vorbis);
119
+ return MA_SUCCESS;
120
+ }
121
+
122
+ static ma_result vorbis_length(ma_data_source *source, ma_uint64 *length) {
123
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
124
+ *length = (ma_uint64)stb_vorbis_stream_length_in_samples(stream->vorbis);
125
+ return MA_SUCCESS;
126
+ }
127
+
128
+ static ma_data_source_vtable vorbis_data_source_vtable = {
129
+ vorbis_read, vorbis_seek, vorbis_data_format, vorbis_cursor, vorbis_length,
130
+ NULL, /* onSetLooping: miniaudio handles looping above this level */
131
+ 0
132
+ };
133
+
134
+ /* ------------------------------------------------------------------------- *
135
+ * Opening
136
+ * ------------------------------------------------------------------------- */
137
+
138
+ /* Shared tail of both entry points: an open stb handle becomes a data source. */
139
+ static ma_result finish_open(rgame_vorbis_stream *stream, void *compressed,
140
+ const ma_allocation_callbacks *allocations,
141
+ ma_data_source **out) {
142
+ stb_vorbis_info info = stb_vorbis_get_info(stream->vorbis);
143
+ stream->channels = (ma_uint32)info.channels;
144
+ stream->sample_rate = info.sample_rate;
145
+ stream->compressed = compressed;
146
+
147
+ ma_data_source_config config = ma_data_source_config_init();
148
+ config.vtable = &vorbis_data_source_vtable;
149
+
150
+ if (ma_data_source_init(&config, &stream->base) != MA_SUCCESS) {
151
+ stb_vorbis_close(stream->vorbis);
152
+ ma_free(compressed, allocations);
153
+ ma_free(stream, allocations);
154
+ return MA_ERROR;
155
+ }
156
+
157
+ *out = (ma_data_source *)stream;
158
+ return MA_SUCCESS;
159
+ }
160
+
161
+ static ma_result vorbis_init_file(void *user_data, const char *path,
162
+ const ma_decoding_backend_config *config,
163
+ const ma_allocation_callbacks *allocations,
164
+ ma_data_source **out) {
165
+ (void)user_data;
166
+ (void)config;
167
+
168
+ rgame_vorbis_stream *stream = ma_malloc(sizeof(rgame_vorbis_stream), allocations);
169
+ if (!stream) {
170
+ return MA_OUT_OF_MEMORY;
171
+ }
172
+
173
+ int error = 0;
174
+ stream->vorbis = stb_vorbis_open_filename(path, &error, NULL);
175
+ if (!stream->vorbis) {
176
+ ma_free(stream, allocations);
177
+ /* Not an error worth distinguishing: a missing file and a file that is
178
+ * not an ogg both mean "this is not a sound", and the caller reports
179
+ * the path either way. */
180
+ return MA_INVALID_FILE;
181
+ }
182
+
183
+ return finish_open(stream, NULL, allocations, out);
184
+ }
185
+
186
+ /*
187
+ * The callback path, which is the one `ma_engine` uses.
188
+ *
189
+ * stb_vorbis cannot read through callbacks, so the whole compressed file is
190
+ * pulled in and opened from memory. See the header for why that is the right
191
+ * trade against the pushdata API.
192
+ */
193
+ static ma_result vorbis_init(void *user_data, ma_read_proc on_read, ma_seek_proc on_seek,
194
+ ma_tell_proc on_tell, void *read_seek_tell_user_data,
195
+ const ma_decoding_backend_config *config,
196
+ const ma_allocation_callbacks *allocations,
197
+ ma_data_source **out) {
198
+ (void)user_data;
199
+ (void)config;
200
+
201
+ /* Seek to the end to find the size, then back. There is no "how big is
202
+ * this" callback; this is the only way to ask. */
203
+ if (on_seek(read_seek_tell_user_data, 0, ma_seek_origin_end) != MA_SUCCESS) {
204
+ return MA_ERROR;
205
+ }
206
+ ma_int64 size = 0;
207
+ /* The `size <= 0` half is belt and braces: stb_vorbis refuses a zero-length
208
+ * buffer on its own, so removing it survives the suite. It stays because
209
+ * `ma_malloc(0)` and a zero-length read are not worth reasoning about at
210
+ * every call site downstream. */
211
+ if (on_tell(read_seek_tell_user_data, &size) != MA_SUCCESS || size <= 0) {
212
+ return MA_ERROR;
213
+ }
214
+ if (on_seek(read_seek_tell_user_data, 0, ma_seek_origin_start) != MA_SUCCESS) {
215
+ return MA_ERROR;
216
+ }
217
+
218
+ void *compressed = ma_malloc((size_t)size, allocations);
219
+ if (!compressed) {
220
+ return MA_OUT_OF_MEMORY;
221
+ }
222
+
223
+ size_t got = 0;
224
+ if (on_read(read_seek_tell_user_data, compressed, (size_t)size, &got) != MA_SUCCESS ||
225
+ got != (size_t)size) {
226
+ ma_free(compressed, allocations);
227
+ return MA_ERROR;
228
+ }
229
+
230
+ rgame_vorbis_stream *stream = ma_malloc(sizeof(rgame_vorbis_stream), allocations);
231
+ if (!stream) {
232
+ ma_free(compressed, allocations);
233
+ return MA_OUT_OF_MEMORY;
234
+ }
235
+
236
+ int error = 0;
237
+ stream->vorbis = stb_vorbis_open_memory(compressed, (int)size, &error, NULL);
238
+ if (!stream->vorbis) {
239
+ ma_free(compressed, allocations);
240
+ ma_free(stream, allocations);
241
+ return MA_INVALID_FILE;
242
+ }
243
+
244
+ /* `compressed` is handed over here, not freed: stb_vorbis keeps pointing
245
+ * into it. */
246
+ return finish_open(stream, compressed, allocations, out);
247
+ }
248
+
249
+ static void vorbis_uninit(void *user_data, ma_data_source *source,
250
+ const ma_allocation_callbacks *allocations) {
251
+ (void)user_data;
252
+ if (!source) {
253
+ return;
254
+ }
255
+
256
+ rgame_vorbis_stream *stream = (rgame_vorbis_stream *)source;
257
+
258
+ /* Close first, free second. Swapping them survives the suite and even the
259
+ * sanitizer, because stb_vorbis_close only releases its own allocations and
260
+ * never looks at the input buffer again — but that is a fact about today's
261
+ * stb_vorbis, not a promise, and the order that cannot be wrong costs
262
+ * nothing. */
263
+ stb_vorbis_close(stream->vorbis);
264
+ ma_free(stream->compressed, allocations); /* NULL when opened by filename */
265
+ ma_data_source_uninit(&stream->base);
266
+ ma_free(stream, allocations);
267
+ }
268
+
269
+ /*
270
+ * Both entry points, though only one is strictly required: with `onInitFile`
271
+ * absent miniaudio falls back to opening the file itself and calling `onInit`,
272
+ * so removing it survives the suite. It stays because that fallback buffers the
273
+ * whole compressed file, and opening by name lets stb_vorbis read from disk
274
+ * instead — which for a music track is a few megabytes not held in memory.
275
+ */
276
+ ma_decoding_backend_vtable rgame_vorbis_decoding_backend = {
277
+ vorbis_init,
278
+ vorbis_init_file,
279
+ NULL, /* onInitFileW: the engine has no wide-character paths */
280
+ NULL, /* onInitMemory: nothing loads an ogg from a Ruby string yet */
281
+ vorbis_uninit
282
+ };
@@ -0,0 +1,45 @@
1
+ #ifndef RGAME_VORBIS_DECODER_H
2
+ #define RGAME_VORBIS_DECODER_H
3
+
4
+ #include "vendor/miniaudio.h"
5
+
6
+ /*
7
+ * Ogg Vorbis for miniaudio, over vendored stb_vorbis.
8
+ *
9
+ * miniaudio cannot read Ogg Vorbis. It decodes wav, mp3 and flac, and vorbis is
10
+ * expected to arrive as a *custom decoding backend* — a small vtable the caller
11
+ * registers. miniaudio ships a reference one, but it uses **system libvorbis**,
12
+ * which would hand back the very dependency the engine avoided by vendoring
13
+ * miniaudio in the first place — the whole reason miniaudio was chosen over
14
+ * SDL_mixer is that it needs nothing installed (vendor/README.md has the
15
+ * comparison). So this is that backend, over stb_vorbis, which is already here
16
+ * and adds no dependency either.
17
+ *
18
+ * ---------------------------------------------------------------------------
19
+ * Why there are two ways in
20
+ * ---------------------------------------------------------------------------
21
+ *
22
+ * A decoding backend can offer `onInitFile`, which is handed a path, and
23
+ * `onInit`, which is handed read/seek/tell callbacks. Both are needed, and
24
+ * finding that out is unpleasant: `ma_decoder_init_file` uses the first, but
25
+ * `ma_engine` — where voices, streaming and mixing live — reads everything
26
+ * through its own virtual file system and calls the second. A backend with only
27
+ * `onInitFile` loads perfectly through `ma_decoder` and then fails inside the
28
+ * engine with a bare `MA_INVALID_FILE` and no indication why.
29
+ *
30
+ * stb_vorbis has no callback-based open: it offers filename, memory, and a
31
+ * pushdata API. So the callback path reads the whole *compressed* file into
32
+ * memory and opens that. For a music track that is a few megabytes — against
33
+ * roughly forty for a full PCM decode of the same three minutes — and the
34
+ * pushdata API, which would avoid even that, is a great deal more machinery
35
+ * than the difference is worth.
36
+ */
37
+
38
+ /*
39
+ * The backend to hand miniaudio, via `ma_decoder_config.ppCustomBackendVTables`
40
+ * or `ma_resource_manager_config.ppCustomDecodingBackendVTables`. Registering
41
+ * it is what makes every `.ogg` path in the engine work.
42
+ */
43
+ extern ma_decoding_backend_vtable rgame_vorbis_decoding_backend;
44
+
45
+ #endif /* RGAME_VORBIS_DECODER_H */
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Manual/visual smoke test for the core extension (layer 3 — the real
4
+ # SDL/GL path). Build it first, then run this from the project root:
5
+ #
6
+ # make ext-core
7
+ # ruby ext/rgame_core/example.rb
8
+ #
9
+ # It opens a window with one of each drawing primitive in it. Arrow keys (or a
10
+ # controller's dpad/stick) move the marker; Escape or closing the window quits.
11
+ # This is the Ruby-side mirror of src/main.c, and it exists to exercise every
12
+ # callback and every draw call the engine offers.
13
+ #
14
+ # Pass a sound file (Ogg Vorbis or WAV) to check audio as well:
15
+ #
16
+ # ruby ext/rgame_core/example.rb assets/theme.ogg
17
+ #
18
+ # Space plays it as a one-shot sample, Return starts and stops it as looping
19
+ # music. There is no asset here to default to, and that is on purpose: this is
20
+ # the only place a *real* sound device is driven — every automated test runs
21
+ # against a null or offline one — so what it is worth checking is your own
22
+ # file, out of your own speakers.
23
+ #
24
+ # The load path points at lib/, not at this directory: `make ext-core`
25
+ # copies the built core_ext.so to lib/rgame/, so this runs against exactly
26
+ # the layout a user of the library would see.
27
+
28
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
29
+ require 'rgame'
30
+ require 'rgame/core'
31
+
32
+ # An App subclass overrides only the hooks it needs; the rest are inherited
33
+ # no-ops, so this class is the smallest useful driver of the engine.
34
+ class Example < RGame::Core::App
35
+ Controls = RGame::Util::Controls
36
+ Color = RGame::Util::Color
37
+
38
+ RED = Color.new(224, 64, 64)
39
+ GREEN = Color.new(64, 224, 96)
40
+ BLUE = Color.new(64, 96, 224)
41
+ YELLOW = Color.new(224, 192, 64)
42
+ TRANSLUCENT_WHITE = Color.new(255, 255, 255, 128)
43
+
44
+ # Frozen and chosen between rather than built: #draw runs sixty times a
45
+ # second, and a string per frame is a garbage collection waiting to happen.
46
+ AUDIO_HINT_NONE = 'audio: pass a sound file on the command line to try it'
47
+ AUDIO_HINT_STOPPED = 'audio: Space plays a sample, Return starts the music'
48
+ AUDIO_HINT_PLAYING = 'audio: Space plays a sample, Return stops the music'
49
+
50
+ attr_reader :frames, :ticks, :cursor_x, :cursor_y
51
+
52
+ def initialize(sound_path = nil)
53
+ super(width: 800, height: 600, caption: 'rgame via Ruby')
54
+ @input = RGame::Core::Input.new(self)
55
+ @pads = RGame::Core::Gamepad.new(self)
56
+ @renderer = RGame::Core::Renderer.new(self)
57
+ @frames = 0
58
+ @ticks = 0
59
+ @cursor_x = 400.0
60
+ @cursor_y = 300.0
61
+ @spin = 0.0
62
+ @baked = nil
63
+ @device = Controls::KEYBOARD
64
+
65
+ # The device is opened whether or not there is anything to play through it,
66
+ # so that starting with no sound card takes the same path as starting with
67
+ # one. Nothing here is tied to the window: audio has no GL context and
68
+ # survives one being recreated.
69
+ @audio = RGame::Core::Audio.new
70
+ puts "audio backend: #{@audio.backend}"
71
+ return unless sound_path
72
+
73
+ @sample = RGame::Core::Sample.new(@audio, sound_path)
74
+ @song = RGame::Core::Song.new(@audio, sound_path)
75
+ end
76
+
77
+ # Once per frame, before the tick batch: pick which device to read. Doing it
78
+ # here rather than per tick is the pattern the engine is shaped around — one
79
+ # sample per frame, reused by however many catch-up ticks follow.
80
+ def frame_begin
81
+ # Poll rather than track: Gamepad answers straight from the engine, so
82
+ # there is no local copy of the connection state to keep in step.
83
+ @device = @pads.connected?(0) ? @pads.device(0) : Controls::KEYBOARD
84
+ end
85
+
86
+ # One fixed simulation tick. dt is always the engine's fixed step.
87
+ def update(dt)
88
+ @ticks += 1
89
+ @spin += dt * 45.0
90
+ speed = 200.0 * dt
91
+ @cursor_x -= speed if @input.down?(:left, device: @device)
92
+ @cursor_x += speed if @input.down?(:right, device: @device)
93
+ @cursor_y -= speed if @input.down?(:up, device: @device)
94
+ @cursor_y += speed if @input.down?(:down, device: @device)
95
+
96
+ # Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
97
+ @cursor_x += @input.axis(:move_x, device: @device) * speed
98
+ @cursor_y += @input.axis(:move_y, device: @device) * speed
99
+ end
100
+
101
+ # One of everything, so a look at the window checks the whole drawing path.
102
+ # Colours are RGame::Util::Color values rather than arrays: a Color is frozen
103
+ # and allocates nothing when it is drawn, which is what per-frame code wants.
104
+ def draw
105
+ r = @renderer
106
+
107
+ # Baked on the first frame and replayed after that: forty rectangles cost
108
+ # one call per frame instead of forty. A real game bakes its tile layers
109
+ # this way. Recording has to happen inside #draw, which is why it is here
110
+ # rather than in initialize.
111
+ @baked ||= r.record do
112
+ 40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
113
+ end
114
+ # Scrolls with the same value the spinning square turns by, so it is
115
+ # visibly the *replay* moving and not a rebake.
116
+ @baked.draw((@spin % 18) - 18, 560)
117
+ r.rect(40, 40, 160, 100, color: RED, z: 0)
118
+ # Higher z wins wherever they overlap, whatever order the calls came in.
119
+ r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
120
+ r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
121
+ r.circle(620, 110, 70, color: BLUE, z: 0)
122
+ r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)
123
+
124
+ # The transform stack: a square turning about its own centre.
125
+ r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }
126
+
127
+ # The clip stack: the rectangle is twice the size of what shows.
128
+ r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }
129
+
130
+ # The cursor the arrow keys move.
131
+ r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)
132
+
133
+ # Text, including accents and punctuation the shipped font has to cover, so
134
+ # a look at the window checks kerning and coverage at once. The second line
135
+ # is centred using text_width, which is what a UI actually does with it.
136
+ r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
137
+ label = format('%d fps', fps)
138
+ r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
139
+ r.text(audio_status, 40, 270 + (r.text_height * 2), color: YELLOW)
140
+
141
+ @frames += 1
142
+ end
143
+
144
+ def button_down(id)
145
+ case id
146
+ when Controls::KEY_ESCAPE then close
147
+ # Held down, this is the overlap check: each press is another voice rather
148
+ # than a restart, so a fast run of them should pile up rather than stutter.
149
+ when Controls::KEY_SPACE then @sample&.play
150
+ when Controls::KEY_RETURN then toggle_music
151
+ end
152
+ end
153
+
154
+ # Return is a toggle, so one key covers both transitions and the "play after
155
+ # stop starts from the beginning" behaviour is audible by pressing it twice.
156
+ def toggle_music
157
+ return unless @song
158
+
159
+ @song.playing? ? @song.stop : @song.play(looping: true)
160
+ end
161
+
162
+ def audio_status
163
+ return AUDIO_HINT_NONE unless @song
164
+
165
+ @song.playing? ? AUDIO_HINT_PLAYING : AUDIO_HINT_STOPPED
166
+ end
167
+
168
+ # The callbacks are for reacting to the change; the polling above is for
169
+ # reading the current state. Slots are stable across a replug, so a pad that
170
+ # falls out and returns comes back as the same player.
171
+ def gamepad_connected(slot)
172
+ puts "controller connected in slot #{slot}: #{@pads.name(slot)}"
173
+ end
174
+
175
+ def gamepad_disconnected(slot)
176
+ puts "controller disconnected from slot #{slot}"
177
+ end
178
+
179
+ def resize(width, height)
180
+ puts "resized to #{width}x#{height}"
181
+ end
182
+ end
183
+
184
+ app = Example.new(ARGV[0])
185
+ app.run
186
+
187
+ puts format('drew %d frames, ran %d ticks, cursor at (%.1f, %.1f), last fps: %.1f',
188
+ app.frames, app.ticks, app.cursor_x, app.cursor_y, app.fps)
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # extconf.rb — run by `ruby extconf.rb` to generate a Makefile via mkmf.
4
+ #
5
+ # mkmf is Ruby's "make makefile" library. It probes the system (headers,
6
+ # libraries, pkg-config) and writes a Makefile that knows how to compile a
7
+ # loadable extension (`core_ext.so`) against the running Ruby's headers.
8
+ # This is the same tool that `gem install` uses under the hood, which is why the
9
+ # root Makefile was written to look like what mkmf emits — the mental model
10
+ # carries straight over.
11
+ #
12
+ # This is the SDL/OpenGL half of the project: everything under RGame::Core.
13
+ # The graphics-free half lives in ../rgame_util (RGame::Util) and links none of
14
+ # these libraries.
15
+
16
+ require 'mkmf'
17
+
18
+ # The engine sources live in this directory rather than a top-level src/:
19
+ # `gem install` unpacks the gem and runs this script, and an extension must be
20
+ # buildable from its own directory.
21
+ #
22
+ # They are grouped into subdirectories by subsystem (app, graphics, text, input,
23
+ # audio) plus ruby/ for the Ruby-facing glue and vendor/ for third-party code.
24
+ # mkmf's default is to compile every .c in the extension's *own* directory and
25
+ # nothing deeper, so the source list has to be spelled out — which is what
26
+ # $srcs and $VPATH below do.
27
+ #
28
+ # $srcs — what to compile. mkmf names each object after the source's
29
+ # *basename*, so the .o files land flat in this directory no matter
30
+ # how deep the .c was. Basenames therefore have to be unique across
31
+ # the subdirectories; mkmf aborts with "source files duplication" if
32
+ # they ever aren't, so that rule enforces itself.
33
+ # $VPATH — where make looks for a source when a rule names it by basename.
34
+ # The generic `.c.o` rule mkmf emits does exactly that, so without
35
+ # this every object would come up as a missing prerequisite.
36
+ #
37
+ # vendor/ contributes only its <name>_impl.c translation units. The libraries
38
+ # themselves are #included *by* those files (stb_vorbis even ships as a .c), so
39
+ # compiling them separately would duplicate every symbol in them.
40
+ SOURCE_DIRS = %w[app graphics text input audio ruby].freeze
41
+
42
+ # Dir.glob sorts its results, so the object list is the same on every machine.
43
+ $srcs = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.c") } +
44
+ Dir.glob("#{$srcdir}/vendor/*_impl.c")
45
+
46
+ (SOURCE_DIRS + %w[vendor]).each { |dir| $VPATH << "$(srcdir)/#{dir}" }
47
+
48
+ # The extension directory itself, so a cross-subsystem include names the folder
49
+ # it comes from: graphics/canvas.c says `#include "graphics/clip.h"`, and text/
50
+ # reaching into graphics/ is visible in the source rather than hidden in a
51
+ # search path. It is also what resolves `#include "vendor/stb_image.h"`.
52
+ # $(srcdir) stays literal here — make expands it, not Ruby.
53
+ $INCFLAGS << ' -I$(srcdir)'
54
+
55
+ # Public API header (include/rgame/core.h), so `#include "rgame/core.h"`
56
+ # resolves.
57
+ $INCFLAGS << ' -I$(srcdir)/include'
58
+
59
+ # RGame::Util's colour header, for the RGBA byte order the renderer writes into
60
+ # every vertex. It is header-only (static inline), so this creates no link
61
+ # dependency between the two extensions — they stay separate .so files. Both
62
+ # directories ship together in the gem, so the relative path holds there too.
63
+ $INCFLAGS << ' -I$(srcdir)/../rgame_util'
64
+
65
+ # SDL2: pkg_config("sdl2") shells out to pkg-config and folds the resulting
66
+ # cflags and libs into mkmf's globals ($CFLAGS/$libs) for us. Returns nil if
67
+ # SDL2 isn't found, so we can fail with a clear message instead of a confusing
68
+ # later compile error.
69
+ abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
70
+
71
+ # OpenGL + libm, matching the link line in the root Makefile. append_library
72
+ # adds `-lGL`/`-lm` in the right spot on the link command.
73
+ #
74
+ # Checked rather than assumed, unlike in the root Makefile. This script is what
75
+ # runs on someone else's machine during `gem install`, and a missing OpenGL
76
+ # development package is a routine thing to hit there. Without a check it
77
+ # surfaces as a linker error about an undefined `glClear` at the end of a long
78
+ # build; with one, the install stops on a sentence naming the package to
79
+ # install. Same reasoning as the SDL2 abort above.
80
+ #
81
+ # The header probed is the one the sources actually include — SDL's own
82
+ # <SDL2/SDL_opengl.h>, which resolves to GL/gl.h or OpenGL/gl.h per platform, so
83
+ # probing GL/gl.h directly would ask the wrong question off Linux. It needs
84
+ # SDL's cflags, which pkg_config folded into $CFLAGS just above.
85
+ abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
86
+
87
+ # `-lGL` is how OpenGL is linked on Linux and the BSDs; macOS wants
88
+ # `-framework OpenGL` instead. Only the first is implemented — as in the root
89
+ # Makefile, which hardcodes -lGL — so this aborts on a platform it cannot link
90
+ # rather than emitting undefined symbols for every gl* call. have_library
91
+ # appends -lGL to $libs itself when the probe succeeds.
92
+ unless have_library('GL', 'glClear')
93
+ abort 'OpenGL library not found (-lGL). Install libgl1-mesa-dev. ' \
94
+ '(macOS needs -framework OpenGL, which is not implemented yet.)'
95
+ end
96
+
97
+ $libs = append_library($libs, 'm')
98
+
99
+ # miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
100
+ # runtime — the property that makes audio cost no new system dependency. Both
101
+ # live in glibc on Linux/BSD. Windows and macOS need neither, and appending them
102
+ # unconditionally would fail the link there, so each is probed first.
103
+ $libs = append_library($libs, 'pthread') if have_library('pthread')
104
+ $libs = append_library($libs, 'dl') if have_library('dl')
105
+
106
+ # Match the project's C standard and warning flags. Note: gnu17, not plain
107
+ # c17 — Ruby's headers occasionally lean on GNU extensions, and gnu17 is a
108
+ # superset of c17 so core.c (which targets c17) still compiles fine.
109
+ $CFLAGS << ' -std=gnu17 -Wall -Wextra'
110
+
111
+ # Emits the Makefile. "rgame/core_ext" -> loaded via
112
+ # `require "rgame/core_ext"`, entry point Init_core_ext (the basename)
113
+ # in core_ext.c. Namespacing it under rgame/ mirrors rgame/util_ext, keeps
114
+ # both extensions off the top of the load path, and — importantly — leaves the
115
+ # bare name "rgame" to lib/rgame.rb, which is the pure-Ruby entry point.
116
+ create_makefile('rgame/core_ext')
117
+
118
+ # The vendored implementations, compiled with warnings off (see
119
+ # vendor/README.md). mkmf has no per-file flag setting, so the rules are
120
+ # appended to the Makefile it just wrote. An explicit rule for a specific
121
+ # target beats mkmf's generic .c.o suffix rule, so these are what get used for
122
+ # those objects and nothing else.
123
+ #
124
+ # Written from one list rather than one block per library: a second hand-copied
125
+ # rule is how the first one drifts. They are explicit rules rather than a `%`
126
+ # pattern because mkmf's Makefiles are meant to work with whatever `make` the
127
+ # platform has, and pattern rules are a GNU extension.
128
+ #
129
+ # `-w` comes last on the command line and switches every warning back off,
130
+ # which is simpler and more robust than trying to subtract -Wall -Wextra from
131
+ # $(CFLAGS) — everything else about how the extension compiles stays identical.
132
+ # Each vendored library's implementation TU (<name>_impl.c) and the file it
133
+ # instantiates. stb_vorbis is the odd one out: it ships as a .c, not a .h.
134
+ VENDORED = {
135
+ 'stb_image' => 'stb_image.h',
136
+ 'stb_truetype' => 'stb_truetype.h',
137
+ 'stb_vorbis' => 'stb_vorbis.c',
138
+ 'miniaudio' => 'miniaudio.h'
139
+ }.freeze
140
+
141
+ File.open('Makefile', 'a') do |makefile|
142
+ VENDORED.each do |name, source|
143
+ makefile.puts <<~MAKE
144
+
145
+ #{name}_impl.#{$OBJEXT}: $(srcdir)/vendor/#{name}_impl.c $(srcdir)/vendor/#{source}
146
+ \t$(ECHO) compiling vendored #{name} with warnings off
147
+ \t$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) -w $(COUTFLAG)$@ -c $(srcdir)/vendor/#{name}_impl.c
148
+ MAKE
149
+ end
150
+
151
+ # Rebuild everything when any of our headers changes.
152
+ #
153
+ # mkmf emits `$(OBJS): $(HDRS)` for this, but it builds HDRS by globbing the
154
+ # extension's own directory only — with the headers a level down in
155
+ # graphics/, text/ and the rest, HDRS comes out empty and editing a header
156
+ # rebuilds *nothing*. That failure is silent: the build succeeds and links
157
+ # objects compiled against the previous version of the struct.
158
+ #
159
+ # One coarse dependency rather than a real per-object dependency scan, for the
160
+ # same reason the root Makefile treats the vendored sources as one list: this
161
+ # is a small project where a full rebuild costs seconds, and a hand-maintained
162
+ # dependency graph is a thing to get quietly wrong.
163
+ headers = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.h") }
164
+ .map { |path| "$(srcdir)/#{path.delete_prefix("#{$srcdir}/")}" }
165
+
166
+ makefile.puts "\n$(OBJS): #{headers.join(' ')}"
167
+ end