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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- 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
|