gemba 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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/THIRD_PARTY_NOTICES +113 -0
  3. data/assets/JetBrainsMonoNL-Regular.ttf +0 -0
  4. data/assets/ark-pixel-12px-monospaced-ja.ttf +0 -0
  5. data/bin/gemba +14 -0
  6. data/ext/gemba/extconf.rb +185 -0
  7. data/ext/gemba/gemba_ext.c +1051 -0
  8. data/ext/gemba/gemba_ext.h +15 -0
  9. data/gemba.gemspec +38 -0
  10. data/lib/gemba/child_window.rb +62 -0
  11. data/lib/gemba/cli.rb +384 -0
  12. data/lib/gemba/config.rb +621 -0
  13. data/lib/gemba/core.rb +121 -0
  14. data/lib/gemba/headless.rb +12 -0
  15. data/lib/gemba/headless_player.rb +206 -0
  16. data/lib/gemba/hotkey_map.rb +202 -0
  17. data/lib/gemba/input_mappings.rb +214 -0
  18. data/lib/gemba/locale.rb +92 -0
  19. data/lib/gemba/locales/en.yml +157 -0
  20. data/lib/gemba/locales/ja.yml +157 -0
  21. data/lib/gemba/method_coverage_service.rb +265 -0
  22. data/lib/gemba/overlay_renderer.rb +109 -0
  23. data/lib/gemba/player.rb +1515 -0
  24. data/lib/gemba/recorder.rb +156 -0
  25. data/lib/gemba/recorder_decoder.rb +325 -0
  26. data/lib/gemba/rom_info_window.rb +346 -0
  27. data/lib/gemba/rom_loader.rb +100 -0
  28. data/lib/gemba/runtime.rb +39 -0
  29. data/lib/gemba/save_state_manager.rb +155 -0
  30. data/lib/gemba/save_state_picker.rb +199 -0
  31. data/lib/gemba/settings_window.rb +1173 -0
  32. data/lib/gemba/tip_service.rb +133 -0
  33. data/lib/gemba/toast_overlay.rb +128 -0
  34. data/lib/gemba/version.rb +5 -0
  35. data/lib/gemba.rb +17 -0
  36. data/test/fixtures/test.gba +0 -0
  37. data/test/fixtures/test.sav +0 -0
  38. data/test/shared/screenshot_helper.rb +113 -0
  39. data/test/shared/simplecov_config.rb +59 -0
  40. data/test/shared/teek_test_worker.rb +388 -0
  41. data/test/shared/tk_test_helper.rb +354 -0
  42. data/test/support/input_mocks.rb +61 -0
  43. data/test/support/player_helpers.rb +77 -0
  44. data/test/test_cli.rb +281 -0
  45. data/test/test_config.rb +897 -0
  46. data/test/test_core.rb +401 -0
  47. data/test/test_gamepad_map.rb +116 -0
  48. data/test/test_headless_player.rb +205 -0
  49. data/test/test_helper.rb +19 -0
  50. data/test/test_hotkey_map.rb +396 -0
  51. data/test/test_keyboard_map.rb +108 -0
  52. data/test/test_locale.rb +159 -0
  53. data/test/test_mgba.rb +26 -0
  54. data/test/test_overlay_renderer.rb +199 -0
  55. data/test/test_player.rb +903 -0
  56. data/test/test_recorder.rb +180 -0
  57. data/test/test_rom_loader.rb +149 -0
  58. data/test/test_save_state_manager.rb +289 -0
  59. data/test/test_settings_hotkeys.rb +434 -0
  60. data/test/test_settings_window.rb +1039 -0
  61. data/test/test_tip_service.rb +138 -0
  62. data/test/test_toast_overlay.rb +216 -0
  63. data/test/test_virtual_keyboard.rb +39 -0
  64. data/test/test_xor_delta.rb +61 -0
  65. metadata +234 -0
@@ -0,0 +1,1051 @@
1
+ #include "gemba_ext.h"
2
+ #include <mgba/core/config.h>
3
+ #include <mgba/core/serialize.h>
4
+ #include <ruby/thread.h>
5
+ #include <string.h>
6
+ #include <stdlib.h>
7
+ #include <math.h>
8
+ #include <fcntl.h>
9
+
10
+ /*
11
+ * Forward declarations for blip_buf (audio buffer API).
12
+ * These functions are part of libmgba but the header may
13
+ * not be in the installed include path.
14
+ */
15
+ struct blip_t;
16
+ int blip_samples_avail(const struct blip_t *);
17
+ int blip_read_samples(struct blip_t *, short out[], int count, int stereo);
18
+ void blip_set_rates(struct blip_t *, double clock_rate, double sample_rate);
19
+
20
+ VALUE mGemba;
21
+ static VALUE cCore;
22
+
23
+ /* No-op logger — prevents segfault when mGBA tries to log
24
+ * without a logger configured (the default is NULL). */
25
+ static void
26
+ null_log(struct mLogger *logger, int category, enum mLogLevel level,
27
+ const char *format, va_list args)
28
+ {
29
+ (void)logger; (void)category; (void)level;
30
+ (void)format; (void)args;
31
+ }
32
+
33
+ static struct mLogger s_null_logger = {
34
+ .log = null_log,
35
+ .filter = NULL,
36
+ };
37
+
38
+ /* GBA key indices (bit positions for set_keys bitmask).
39
+ * Matches mGBA's GBA_KEY_* enum. */
40
+ #define GEMBA_KEY_A 0
41
+ #define GEMBA_KEY_B 1
42
+ #define GEMBA_KEY_SELECT 2
43
+ #define GEMBA_KEY_START 3
44
+ #define GEMBA_KEY_RIGHT 4
45
+ #define GEMBA_KEY_LEFT 5
46
+ #define GEMBA_KEY_UP 6
47
+ #define GEMBA_KEY_DOWN 7
48
+ #define GEMBA_KEY_R 8
49
+ #define GEMBA_KEY_L 9
50
+
51
+ /* --------------------------------------------------------- */
52
+ /* Core wrapper struct */
53
+ /* --------------------------------------------------------- */
54
+
55
+ /* --------------------------------------------------------- */
56
+ /* GBA color correction (Pokefan531 / Color Mangler formula) */
57
+ /* */
58
+ /* The GBA LCD has a non-standard gamma (~3.2) and channel */
59
+ /* cross-talk. Games were designed with exaggerated colors */
60
+ /* to compensate. This LUT maps raw mGBA ARGB8888 output to */
61
+ /* corrected sRGB values that approximate the original GBA */
62
+ /* LCD appearance. */
63
+ /* */
64
+ /* 32x32x32 entries (one per RGB555 input color) = 128KB. */
65
+ /* Built once on enable; applied per-pixel in video_buffer_argb. */
66
+ /* */
67
+ /* Reference: libretro gba-color.glsl (public domain) */
68
+ /* https://github.com/libretro/glsl-shaders/blob/master/ */
69
+ /* handheld/shaders/color/gba-color.glsl */
70
+ /* --------------------------------------------------------- */
71
+
72
+ static uint32_t gba_color_lut[32][32][32];
73
+ static int gba_color_lut_built = 0;
74
+
75
+ static void
76
+ build_gba_color_lut(void)
77
+ {
78
+ const double target_gamma = 2.2;
79
+ const double darken_screen = 1.0;
80
+ const double display_gamma = 2.2;
81
+ const double lum = 0.94;
82
+ const double input_gamma = target_gamma + darken_screen; /* 3.2 */
83
+
84
+ for (int ri = 0; ri < 32; ri++) {
85
+ for (int gi = 0; gi < 32; gi++) {
86
+ for (int bi = 0; bi < 32; bi++) {
87
+ double r = pow(ri / 31.0, input_gamma) * lum;
88
+ double g = pow(gi / 31.0, input_gamma) * lum;
89
+ double b = pow(bi / 31.0, input_gamma) * lum;
90
+ if (r > 1.0) r = 1.0;
91
+ if (g > 1.0) g = 1.0;
92
+ if (b > 1.0) b = 1.0;
93
+
94
+ /* Pokefan531 mixing matrix */
95
+ double nr = 0.82 * r + 0.125 * g + 0.195 * b;
96
+ double ng = 0.24 * r + 0.665 * g + 0.075 * b;
97
+ double nb = -0.06 * r + 0.21 * g + 0.73 * b;
98
+
99
+ if (nr < 0.0) nr = 0.0; if (nr > 1.0) nr = 1.0;
100
+ if (ng < 0.0) ng = 0.0; if (ng > 1.0) ng = 1.0;
101
+ if (nb < 0.0) nb = 0.0; if (nb > 1.0) nb = 1.0;
102
+
103
+ nr = pow(nr, 1.0 / display_gamma);
104
+ ng = pow(ng, 1.0 / display_gamma);
105
+ nb = pow(nb, 1.0 / display_gamma);
106
+
107
+ uint8_t or8 = (uint8_t)(nr * 255.0 + 0.5);
108
+ uint8_t og8 = (uint8_t)(ng * 255.0 + 0.5);
109
+ uint8_t ob8 = (uint8_t)(nb * 255.0 + 0.5);
110
+
111
+ gba_color_lut[ri][gi][bi] =
112
+ 0xFF000000 | ((uint32_t)or8 << 16) |
113
+ ((uint32_t)og8 << 8) | (uint32_t)ob8;
114
+ }
115
+ }
116
+ }
117
+ gba_color_lut_built = 1;
118
+ }
119
+
120
+ /* Apply LUT to an ARGB8888 pixel. The GBA only outputs 15-bit color
121
+ * (RGB555), so we quantize each 8-bit channel to 5 bits for lookup. */
122
+ static inline uint32_t
123
+ color_correct_pixel(uint32_t argb)
124
+ {
125
+ int r5 = (int)((argb >> 16) & 0xFF) >> 3;
126
+ int g5 = (int)((argb >> 8) & 0xFF) >> 3;
127
+ int b5 = (int)((argb ) & 0xFF) >> 3;
128
+ return gba_color_lut[r5][g5][b5];
129
+ }
130
+
131
+ struct mgba_core {
132
+ struct mCore *core;
133
+ color_t *video_buffer;
134
+ uint32_t *prev_frame;
135
+ int width;
136
+ int height;
137
+ int destroyed;
138
+ int color_correction;
139
+ int frame_blending;
140
+ /* Rewind ring buffer */
141
+ int rewind_capacity; /* number of slots (0 = disabled) */
142
+ int rewind_head; /* next write index */
143
+ int rewind_count; /* number of valid snapshots */
144
+ size_t rewind_state_size; /* bytes per snapshot */
145
+ void **rewind_slots; /* array of rewind_capacity void* buffers */
146
+ };
147
+
148
+ static void
149
+ mgba_rewind_free(struct mgba_core *mc)
150
+ {
151
+ if (mc->rewind_slots) {
152
+ for (int i = 0; i < mc->rewind_capacity; i++) {
153
+ if (mc->rewind_slots[i]) {
154
+ free(mc->rewind_slots[i]);
155
+ mc->rewind_slots[i] = NULL;
156
+ }
157
+ }
158
+ free(mc->rewind_slots);
159
+ mc->rewind_slots = NULL;
160
+ }
161
+ mc->rewind_capacity = 0;
162
+ mc->rewind_head = 0;
163
+ mc->rewind_count = 0;
164
+ mc->rewind_state_size = 0;
165
+ }
166
+
167
+ static void
168
+ mgba_core_cleanup(struct mgba_core *mc)
169
+ {
170
+ mgba_rewind_free(mc);
171
+ if (!mc->destroyed && mc->core) {
172
+ mc->core->deinit(mc->core);
173
+ mc->core = NULL;
174
+ }
175
+ if (mc->video_buffer) {
176
+ free(mc->video_buffer);
177
+ mc->video_buffer = NULL;
178
+ }
179
+ if (mc->prev_frame) {
180
+ free(mc->prev_frame);
181
+ mc->prev_frame = NULL;
182
+ }
183
+ mc->destroyed = 1;
184
+ }
185
+
186
+ static void
187
+ mgba_core_dfree(void *ptr)
188
+ {
189
+ struct mgba_core *mc = ptr;
190
+ mgba_core_cleanup(mc);
191
+ xfree(mc);
192
+ }
193
+
194
+ static size_t
195
+ mgba_core_memsize(const void *ptr)
196
+ {
197
+ const struct mgba_core *mc = ptr;
198
+ size_t size = sizeof(struct mgba_core);
199
+ if (mc->video_buffer) {
200
+ size += (size_t)mc->width * mc->height * sizeof(color_t);
201
+ }
202
+ if (mc->prev_frame) {
203
+ size += (size_t)mc->width * mc->height * sizeof(uint32_t);
204
+ }
205
+ if (mc->rewind_slots) {
206
+ size += (size_t)mc->rewind_capacity * mc->rewind_state_size;
207
+ size += (size_t)mc->rewind_capacity * sizeof(void *);
208
+ }
209
+ return size;
210
+ }
211
+
212
+ static const rb_data_type_t mgba_core_type = {
213
+ .wrap_struct_name = "TeekMGBA::Core",
214
+ .function = {
215
+ .dmark = NULL,
216
+ .dfree = mgba_core_dfree,
217
+ .dsize = mgba_core_memsize,
218
+ },
219
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
220
+ };
221
+
222
+ static VALUE
223
+ mgba_core_alloc(VALUE klass)
224
+ {
225
+ struct mgba_core *mc;
226
+ VALUE obj = TypedData_Make_Struct(klass, struct mgba_core,
227
+ &mgba_core_type, mc);
228
+ mc->core = NULL;
229
+ mc->video_buffer = NULL;
230
+ mc->prev_frame = NULL;
231
+ mc->width = 0;
232
+ mc->height = 0;
233
+ mc->destroyed = 0;
234
+ mc->color_correction = 0;
235
+ mc->frame_blending = 0;
236
+ mc->rewind_capacity = 0;
237
+ mc->rewind_head = 0;
238
+ mc->rewind_count = 0;
239
+ mc->rewind_state_size = 0;
240
+ mc->rewind_slots = NULL;
241
+ return obj;
242
+ }
243
+
244
+ static struct mgba_core *
245
+ get_mgba_core(VALUE self)
246
+ {
247
+ struct mgba_core *mc;
248
+ TypedData_Get_Struct(self, struct mgba_core, &mgba_core_type, mc);
249
+ if (mc->destroyed || !mc->core) {
250
+ rb_raise(rb_eRuntimeError, "mGBA core has been destroyed");
251
+ }
252
+ return mc;
253
+ }
254
+
255
+ /* --------------------------------------------------------- */
256
+ /* Core#initialize(rom_path, save_dir=nil) */
257
+ /* --------------------------------------------------------- */
258
+
259
+ static VALUE
260
+ mgba_core_initialize(int argc, VALUE *argv, VALUE self)
261
+ {
262
+ VALUE rom_path, save_dir;
263
+ rb_scan_args(argc, argv, "11", &rom_path, &save_dir);
264
+
265
+ struct mgba_core *mc;
266
+ TypedData_Get_Struct(self, struct mgba_core, &mgba_core_type, mc);
267
+
268
+ Check_Type(rom_path, T_STRING);
269
+ const char *path = StringValueCStr(rom_path);
270
+
271
+ /* 1. Detect platform from ROM */
272
+ struct mCore *core = mCoreFind(path);
273
+ if (!core) {
274
+ rb_raise(rb_eArgError, "mCoreFind failed — unsupported ROM: %s", path);
275
+ }
276
+
277
+ /* 2. Initialize core + config (required per mGBA Python bindings) */
278
+ if (!core->init(core)) {
279
+ rb_raise(rb_eRuntimeError, "mCore init failed");
280
+ }
281
+ mCoreInitConfig(core, NULL);
282
+
283
+ /* 3. Get desired video dimensions */
284
+ unsigned w, h;
285
+ core->desiredVideoDimensions(core, &w, &h);
286
+ mc->width = (int)w;
287
+ mc->height = (int)h;
288
+
289
+ /* 4. Allocate and set video buffer */
290
+ mc->video_buffer = calloc((size_t)w * h, sizeof(color_t));
291
+ if (!mc->video_buffer) {
292
+ core->deinit(core);
293
+ rb_raise(rb_eNoMemError, "failed to allocate video buffer");
294
+ }
295
+ core->setVideoBuffer(core, mc->video_buffer, w);
296
+
297
+ /* 4b. Allocate previous-frame buffer for frame blending */
298
+ mc->prev_frame = calloc((size_t)w * h, sizeof(uint32_t));
299
+ if (!mc->prev_frame) {
300
+ free(mc->video_buffer);
301
+ mc->video_buffer = NULL;
302
+ core->deinit(core);
303
+ rb_raise(rb_eNoMemError, "failed to allocate prev_frame buffer");
304
+ }
305
+
306
+ /* 5. Set audio buffer size */
307
+ core->setAudioBufferSize(core, 2048);
308
+
309
+ /* 6. Load ROM (convenience function handles VFile internally) */
310
+ if (!mCoreLoadFile(core, path)) {
311
+ free(mc->video_buffer);
312
+ mc->video_buffer = NULL;
313
+ free(mc->prev_frame);
314
+ mc->prev_frame = NULL;
315
+ core->deinit(core);
316
+ rb_raise(rb_eArgError, "failed to load ROM: %s", path);
317
+ }
318
+
319
+ /* 7. Override save directory if provided */
320
+ if (!NIL_P(save_dir)) {
321
+ Check_Type(save_dir, T_STRING);
322
+ struct mCoreOptions opts = { 0 };
323
+ opts.savegamePath = (char *)StringValueCStr(save_dir);
324
+ mDirectorySetMapOptions(&core->dirs, &opts);
325
+ }
326
+
327
+ /* 8. Reset */
328
+ core->reset(core);
329
+
330
+ /* 9. Autoload save file (.sav alongside ROM, or in save_dir).
331
+ * Creates the .sav if it doesn't exist yet. */
332
+ mCoreAutoloadSave(core);
333
+
334
+ /* 10. Set blip_buf output rate to 44100 Hz (must be after reset) */
335
+ {
336
+ double clock_rate = (double)core->frequency(core);
337
+ struct blip_t *left = core->getAudioChannel(core, 0);
338
+ struct blip_t *right = core->getAudioChannel(core, 1);
339
+ if (!left || !right) {
340
+ free(mc->video_buffer);
341
+ mc->video_buffer = NULL;
342
+ free(mc->prev_frame);
343
+ mc->prev_frame = NULL;
344
+ core->deinit(core);
345
+ rb_raise(rb_eRuntimeError, "mGBA audio channels not available");
346
+ }
347
+ blip_set_rates(left, clock_rate, 44100.0);
348
+ blip_set_rates(right, clock_rate, 44100.0);
349
+ }
350
+
351
+ mc->core = core;
352
+ return self;
353
+ }
354
+
355
+ /* --------------------------------------------------------- */
356
+ /* Core#run_frame — releases GVL for ~16ms of CPU work */
357
+ /* --------------------------------------------------------- */
358
+
359
+ struct run_frame_args {
360
+ struct mCore *core;
361
+ };
362
+
363
+ static void *
364
+ run_frame_nogvl(void *arg)
365
+ {
366
+ struct run_frame_args *a = arg;
367
+ a->core->runFrame(a->core);
368
+ return NULL;
369
+ }
370
+
371
+ static VALUE
372
+ mgba_core_run_frame(VALUE self)
373
+ {
374
+ struct mgba_core *mc = get_mgba_core(self);
375
+ struct run_frame_args args = { .core = mc->core };
376
+ rb_thread_call_without_gvl(run_frame_nogvl, &args, RUBY_UBF_IO, NULL);
377
+ return Qnil;
378
+ }
379
+
380
+ /* --------------------------------------------------------- */
381
+ /* Core#video_buffer */
382
+ /* --------------------------------------------------------- */
383
+
384
+ static VALUE
385
+ mgba_core_video_buffer(VALUE self)
386
+ {
387
+ struct mgba_core *mc = get_mgba_core(self);
388
+ long size = (long)mc->width * mc->height * (long)sizeof(color_t);
389
+ return rb_str_new((const char *)mc->video_buffer, size);
390
+ }
391
+
392
+ /* --------------------------------------------------------- */
393
+ /* Core#video_buffer_argb */
394
+ /* Returns pixel data with R↔B swapped for SDL ARGB8888. */
395
+ /* mGBA color_t is 0xAABBGGRR; SDL wants 0xAARRGGBB. */
396
+ /* --------------------------------------------------------- */
397
+
398
+ static VALUE
399
+ mgba_core_video_buffer_argb(VALUE self)
400
+ {
401
+ struct mgba_core *mc = get_mgba_core(self);
402
+ long npixels = (long)mc->width * mc->height;
403
+ long size = npixels * (long)sizeof(uint32_t);
404
+ VALUE str = rb_str_new(NULL, size);
405
+ uint32_t *dst = (uint32_t *)RSTRING_PTR(str);
406
+ const uint32_t *src = (const uint32_t *)mc->video_buffer;
407
+
408
+ if (mc->color_correction && !gba_color_lut_built)
409
+ build_gba_color_lut();
410
+
411
+ for (long i = 0; i < npixels; i++) {
412
+ uint32_t px = src[i];
413
+ /* mGBA native color_t is mCOLOR_XBGR8 (0xXXBBGGRR) — the high
414
+ * byte is unused padding, not alpha. Force it to 0xFF so
415
+ * consumers that interpret byte 3 as alpha (Tk photo, PNG)
416
+ * don't get transparent pixels.
417
+ * Ref: https://github.com/mgba-emu/mgba/blob/c30aaa8f42b5b786924d955630b29cd990176968/include/mgba-util/image.h#L62 */
418
+ uint32_t argb = 0xFF000000
419
+ | ((px & 0x000000FF) << 16)
420
+ | (px & 0x0000FF00)
421
+ | ((px & 0x00FF0000) >> 16);
422
+
423
+ if (mc->color_correction)
424
+ argb = color_correct_pixel(argb);
425
+
426
+ if (mc->frame_blending && mc->prev_frame) {
427
+ uint32_t prev = mc->prev_frame[i];
428
+ mc->prev_frame[i] = argb; /* store unblended for next frame */
429
+ argb = ((argb & 0xFEFEFEFE) >> 1)
430
+ + ((prev & 0xFEFEFEFE) >> 1)
431
+ + (argb & prev & 0x01010101);
432
+ }
433
+
434
+ dst[i] = argb;
435
+ }
436
+ return str;
437
+ }
438
+
439
+ /* --------------------------------------------------------- */
440
+ /* Core#audio_buffer */
441
+ /* --------------------------------------------------------- */
442
+
443
+ static VALUE
444
+ mgba_core_audio_buffer(VALUE self)
445
+ {
446
+ struct mgba_core *mc = get_mgba_core(self);
447
+
448
+ struct blip_t *left = mc->core->getAudioChannel(mc->core, 0);
449
+ struct blip_t *right = mc->core->getAudioChannel(mc->core, 1);
450
+ if (!left || !right) {
451
+ return rb_str_new(NULL, 0);
452
+ }
453
+
454
+ int avail = blip_samples_avail(left);
455
+ if (avail <= 0) {
456
+ return rb_str_new(NULL, 0);
457
+ }
458
+
459
+ /* Interleaved stereo int16: L R L R ... */
460
+ long byte_size = (long)avail * 2 * (long)sizeof(int16_t);
461
+ VALUE str = rb_str_new(NULL, byte_size);
462
+ int16_t *buf = (int16_t *)RSTRING_PTR(str);
463
+
464
+ /* stereo=1: write every other sample for interleaving */
465
+ blip_read_samples(left, buf, avail, 1);
466
+ blip_read_samples(right, buf + 1, avail, 1);
467
+
468
+ return str;
469
+ }
470
+
471
+ /* --------------------------------------------------------- */
472
+ /* Core#set_keys(bitmask) */
473
+ /* --------------------------------------------------------- */
474
+
475
+ static VALUE
476
+ mgba_core_set_keys(VALUE self, VALUE keys)
477
+ {
478
+ struct mgba_core *mc = get_mgba_core(self);
479
+ uint32_t bitmask = NUM2UINT(keys);
480
+ mc->core->setKeys(mc->core, bitmask);
481
+ return Qnil;
482
+ }
483
+
484
+ /* --------------------------------------------------------- */
485
+ /* Core#width, Core#height */
486
+ /* --------------------------------------------------------- */
487
+
488
+ static VALUE
489
+ mgba_core_width(VALUE self)
490
+ {
491
+ struct mgba_core *mc = get_mgba_core(self);
492
+ return INT2NUM(mc->width);
493
+ }
494
+
495
+ static VALUE
496
+ mgba_core_height(VALUE self)
497
+ {
498
+ struct mgba_core *mc = get_mgba_core(self);
499
+ return INT2NUM(mc->height);
500
+ }
501
+
502
+ /* --------------------------------------------------------- */
503
+ /* Core#title */
504
+ /* --------------------------------------------------------- */
505
+
506
+ static VALUE
507
+ mgba_core_title(VALUE self)
508
+ {
509
+ struct mgba_core *mc = get_mgba_core(self);
510
+ char title[16];
511
+ memset(title, 0, sizeof(title));
512
+ mc->core->getGameTitle(mc->core, title);
513
+ title[15] = '\0';
514
+
515
+ /* strlen stops at first null; then trim trailing spaces */
516
+ int len = (int)strlen(title);
517
+ while (len > 0 && title[len - 1] == ' ') len--;
518
+ return rb_str_new(title, len);
519
+ }
520
+
521
+ /* --------------------------------------------------------- */
522
+ /* Core#game_code */
523
+ /* --------------------------------------------------------- */
524
+
525
+ static VALUE
526
+ mgba_core_game_code(VALUE self)
527
+ {
528
+ struct mgba_core *mc = get_mgba_core(self);
529
+ char code[16];
530
+ memset(code, 0, sizeof(code));
531
+ mc->core->getGameCode(mc->core, code);
532
+ code[15] = '\0';
533
+
534
+ /* strlen stops at first null; then trim trailing spaces */
535
+ int len = (int)strlen(code);
536
+ while (len > 0 && code[len - 1] == ' ') len--;
537
+ return rb_str_new(code, len);
538
+ }
539
+
540
+ /* --------------------------------------------------------- */
541
+ /* Core#checksum */
542
+ /* Returns the CRC32 checksum of the loaded ROM. */
543
+ /* --------------------------------------------------------- */
544
+
545
+ static VALUE
546
+ mgba_core_checksum(VALUE self)
547
+ {
548
+ struct mgba_core *mc = get_mgba_core(self);
549
+ uint32_t crc = 0;
550
+ mc->core->checksum(mc->core, &crc, mCHECKSUM_CRC32);
551
+ return UINT2NUM(crc);
552
+ }
553
+
554
+ /* --------------------------------------------------------- */
555
+ /* Core#platform */
556
+ /* Returns "GBA", "GB", or "Unknown". */
557
+ /* --------------------------------------------------------- */
558
+
559
+ static VALUE
560
+ mgba_core_platform(VALUE self)
561
+ {
562
+ struct mgba_core *mc = get_mgba_core(self);
563
+ enum mPlatform p = mc->core->platform(mc->core);
564
+ switch (p) {
565
+ case mPLATFORM_GBA: return rb_str_new_cstr("GBA");
566
+ case mPLATFORM_GB: return rb_str_new_cstr("GB");
567
+ default: return rb_str_new_cstr("Unknown");
568
+ }
569
+ }
570
+
571
+ /* --------------------------------------------------------- */
572
+ /* Core#rom_size */
573
+ /* --------------------------------------------------------- */
574
+
575
+ static VALUE
576
+ mgba_core_rom_size(VALUE self)
577
+ {
578
+ struct mgba_core *mc = get_mgba_core(self);
579
+ size_t sz = mc->core->romSize(mc->core);
580
+ return SIZET2NUM(sz);
581
+ }
582
+
583
+ /* --------------------------------------------------------- */
584
+ /* Core#maker_code */
585
+ /* Reads the 2-byte maker/publisher code from the GBA ROM */
586
+ /* header at offset 0xB0. Uses busRead8 at 0x080000B0. */
587
+ /* Returns empty string for non-GBA ROMs. */
588
+ /* --------------------------------------------------------- */
589
+
590
+ static VALUE
591
+ mgba_core_maker_code(VALUE self)
592
+ {
593
+ struct mgba_core *mc = get_mgba_core(self);
594
+ if (mc->core->platform(mc->core) != mPLATFORM_GBA) {
595
+ return rb_str_new_cstr("");
596
+ }
597
+
598
+ char maker[3];
599
+ maker[0] = (char)mc->core->busRead8(mc->core, 0x080000B0);
600
+ maker[1] = (char)mc->core->busRead8(mc->core, 0x080000B1);
601
+ maker[2] = '\0';
602
+ return rb_str_new(maker, (int)strlen(maker));
603
+ }
604
+
605
+ /* --------------------------------------------------------- */
606
+ /* Core#save_state_to_file(path) */
607
+ /* Save the complete emulator state to a file. */
608
+ /* Returns true on success, false on failure. */
609
+ /* --------------------------------------------------------- */
610
+
611
+ static VALUE
612
+ mgba_core_save_state_to_file(VALUE self, VALUE rb_path)
613
+ {
614
+ struct mgba_core *mc = get_mgba_core(self);
615
+ Check_Type(rb_path, T_STRING);
616
+ const char *path = StringValueCStr(rb_path);
617
+
618
+ struct VFile *vf = VFileOpen(path, O_CREAT | O_TRUNC | O_WRONLY);
619
+ if (!vf) {
620
+ rb_raise(rb_eRuntimeError, "Cannot open state file for writing: %s", path);
621
+ }
622
+
623
+ bool ok = mCoreSaveStateNamed(mc->core, vf, SAVESTATE_ALL);
624
+ vf->close(vf);
625
+ return ok ? Qtrue : Qfalse;
626
+ }
627
+
628
+ /* --------------------------------------------------------- */
629
+ /* Core#load_state_from_file(path) */
630
+ /* Load emulator state from a file. */
631
+ /* Returns true on success, false on failure. */
632
+ /* --------------------------------------------------------- */
633
+
634
+ static VALUE
635
+ mgba_core_load_state_from_file(VALUE self, VALUE rb_path)
636
+ {
637
+ struct mgba_core *mc = get_mgba_core(self);
638
+ Check_Type(rb_path, T_STRING);
639
+ const char *path = StringValueCStr(rb_path);
640
+
641
+ struct VFile *vf = VFileOpen(path, O_RDONLY);
642
+ if (!vf) {
643
+ return Qfalse;
644
+ }
645
+
646
+ bool ok = mCoreLoadStateNamed(mc->core, vf, SAVESTATE_ALL);
647
+ vf->close(vf);
648
+ return ok ? Qtrue : Qfalse;
649
+ }
650
+
651
+ /* --------------------------------------------------------- */
652
+ /* Core#color_correction=, Core#color_correction? */
653
+ /* --------------------------------------------------------- */
654
+
655
+ static VALUE
656
+ mgba_core_set_color_correction(VALUE self, VALUE val)
657
+ {
658
+ struct mgba_core *mc = get_mgba_core(self);
659
+ mc->color_correction = RTEST(val) ? 1 : 0;
660
+ if (mc->color_correction && !gba_color_lut_built) {
661
+ build_gba_color_lut();
662
+ }
663
+ return val;
664
+ }
665
+
666
+ static VALUE
667
+ mgba_core_color_correction_p(VALUE self)
668
+ {
669
+ struct mgba_core *mc = get_mgba_core(self);
670
+ return mc->color_correction ? Qtrue : Qfalse;
671
+ }
672
+
673
+ /* --------------------------------------------------------- */
674
+ /* Core#frame_blending=, Core#frame_blending? */
675
+ /* --------------------------------------------------------- */
676
+
677
+ static VALUE
678
+ mgba_core_set_frame_blending(VALUE self, VALUE val)
679
+ {
680
+ struct mgba_core *mc = get_mgba_core(self);
681
+ mc->frame_blending = RTEST(val) ? 1 : 0;
682
+ return val;
683
+ }
684
+
685
+ static VALUE
686
+ mgba_core_frame_blending_p(VALUE self)
687
+ {
688
+ struct mgba_core *mc = get_mgba_core(self);
689
+ return mc->frame_blending ? Qtrue : Qfalse;
690
+ }
691
+
692
+ /* --------------------------------------------------------- */
693
+ /* Rewind ring buffer */
694
+ /* --------------------------------------------------------- */
695
+
696
+ /*
697
+ * Core#rewind_init(capacity)
698
+ * Allocate a ring buffer of `capacity` state snapshots.
699
+ * Each slot is core->stateSize() bytes. Frees any existing buffer.
700
+ */
701
+ static VALUE
702
+ mgba_core_rewind_init(VALUE self, VALUE rb_capacity)
703
+ {
704
+ struct mgba_core *mc = get_mgba_core(self);
705
+ int capacity = NUM2INT(rb_capacity);
706
+ if (capacity <= 0)
707
+ rb_raise(rb_eArgError, "rewind capacity must be positive");
708
+
709
+ /* Free existing rewind buffer if reinitializing */
710
+ mgba_rewind_free(mc);
711
+
712
+ size_t state_size = mc->core->stateSize(mc->core);
713
+ void **slots = calloc((size_t)capacity, sizeof(void *));
714
+ if (!slots)
715
+ rb_raise(rb_eNoMemError, "failed to allocate rewind slot array");
716
+
717
+ for (int i = 0; i < capacity; i++) {
718
+ slots[i] = malloc(state_size);
719
+ if (!slots[i]) {
720
+ /* Clean up already-allocated slots */
721
+ for (int j = 0; j < i; j++) free(slots[j]);
722
+ free(slots);
723
+ rb_raise(rb_eNoMemError, "failed to allocate rewind slot %d", i);
724
+ }
725
+ }
726
+
727
+ mc->rewind_capacity = capacity;
728
+ mc->rewind_state_size = state_size;
729
+ mc->rewind_slots = slots;
730
+ mc->rewind_head = 0;
731
+ mc->rewind_count = 0;
732
+ return Qnil;
733
+ }
734
+
735
+ /*
736
+ * Core#rewind_deinit
737
+ * Free all rewind buffers.
738
+ */
739
+ static VALUE
740
+ mgba_core_rewind_deinit(VALUE self)
741
+ {
742
+ struct mgba_core *mc = get_mgba_core(self);
743
+ mgba_rewind_free(mc);
744
+ return Qnil;
745
+ }
746
+
747
+ /*
748
+ * Core#rewind_push
749
+ * Save current state into the next ring buffer slot.
750
+ * Returns true on success, false if rewind not initialized.
751
+ */
752
+ static VALUE
753
+ mgba_core_rewind_push(VALUE self)
754
+ {
755
+ struct mgba_core *mc = get_mgba_core(self);
756
+ if (!mc->rewind_slots || mc->rewind_capacity <= 0)
757
+ return Qfalse;
758
+
759
+ mc->core->saveState(mc->core, mc->rewind_slots[mc->rewind_head]);
760
+ mc->rewind_head = (mc->rewind_head + 1) % mc->rewind_capacity;
761
+ if (mc->rewind_count < mc->rewind_capacity)
762
+ mc->rewind_count++;
763
+ return Qtrue;
764
+ }
765
+
766
+ /*
767
+ * Core#rewind_pop
768
+ * Load the oldest snapshot and clear the buffer.
769
+ * Jumps back to the earliest saved point (~N seconds ago).
770
+ * Returns true on success, false if no snapshots available.
771
+ */
772
+ static VALUE
773
+ mgba_core_rewind_pop(VALUE self)
774
+ {
775
+ struct mgba_core *mc = get_mgba_core(self);
776
+ if (!mc->rewind_slots || mc->rewind_count <= 0)
777
+ return Qfalse;
778
+
779
+ /* oldest = head - count (wrapped) */
780
+ int oldest = (mc->rewind_head - mc->rewind_count + mc->rewind_capacity)
781
+ % mc->rewind_capacity;
782
+ mc->core->loadState(mc->core, mc->rewind_slots[oldest]);
783
+ mc->rewind_head = 0;
784
+ mc->rewind_count = 0;
785
+ return Qtrue;
786
+ }
787
+
788
+ /*
789
+ * Core#rewind_count
790
+ * Returns the number of valid snapshots in the buffer.
791
+ */
792
+ static VALUE
793
+ mgba_core_rewind_count(VALUE self)
794
+ {
795
+ struct mgba_core *mc = get_mgba_core(self);
796
+ return INT2NUM(mc->rewind_count);
797
+ }
798
+
799
+ /* --------------------------------------------------------- */
800
+ /* Core#destroy, Core#destroyed? */
801
+ /* --------------------------------------------------------- */
802
+
803
+ static VALUE
804
+ mgba_core_destroy(VALUE self)
805
+ {
806
+ struct mgba_core *mc;
807
+ TypedData_Get_Struct(self, struct mgba_core, &mgba_core_type, mc);
808
+ mgba_core_cleanup(mc);
809
+ return Qnil;
810
+ }
811
+
812
+ static VALUE
813
+ mgba_core_destroyed_p(VALUE self)
814
+ {
815
+ struct mgba_core *mc;
816
+ TypedData_Get_Struct(self, struct mgba_core, &mgba_core_type, mc);
817
+ return mc->destroyed ? Qtrue : Qfalse;
818
+ }
819
+
820
+ /* --------------------------------------------------------- */
821
+ /* Gemba.toast_background(w, h, radius) */
822
+ /* */
823
+ /* Generates ARGB8888 pixel data for a toast notification */
824
+ /* background with rounded corners. For each pixel we */
825
+ /* compute a signed distance to the rounded-rect edge, then */
826
+ /* assign one of four zones based on that distance: */
827
+ /* outside → transparent */
828
+ /* outer fringe → border color fading in (anti-alias) */
829
+ /* border band → solid border color */
830
+ /* inner fringe → border blending to fill (anti-alias) */
831
+ /* interior → solid fill color */
832
+ /* Returns a binary String of w*h*4 bytes. */
833
+ /* --------------------------------------------------------- */
834
+
835
+ /* Toast palette (non-premultiplied, for SDL_BLENDMODE_BLEND)
836
+ * Fill: near-black, slight blue tint — readable over game art
837
+ * Border: blue-grey — subtle edge visible against dark scenes
838
+ * Alpha is 0–255: 180/255 ≈ 70% opaque, 210/255 ≈ 82% opaque */
839
+ enum {
840
+ TOAST_FILL_R = 20, TOAST_FILL_G = 20, TOAST_FILL_B = 28, TOAST_FILL_A = 180,
841
+ TOAST_BDR_R = 100, TOAST_BDR_G = 110, TOAST_BDR_B = 140, TOAST_BDR_A = 210,
842
+ };
843
+
844
+ static inline uint32_t
845
+ toast_argb(uint8_t a, uint8_t r, uint8_t g, uint8_t b) {
846
+ return ((uint32_t)a << 24) | ((uint32_t)r << 16) |
847
+ ((uint32_t)g << 8) | (uint32_t)b;
848
+ }
849
+
850
+ static VALUE
851
+ mgba_toast_background(VALUE mod, VALUE rb_w, VALUE rb_h, VALUE rb_rad)
852
+ {
853
+ (void)mod;
854
+ int w = NUM2INT(rb_w);
855
+ int h = NUM2INT(rb_h);
856
+ int rad = NUM2INT(rb_rad);
857
+
858
+ if (w <= 0 || h <= 0) return rb_str_new(NULL, 0);
859
+ if (rad < 0) rad = 0;
860
+ if (rad > w / 2) rad = w / 2;
861
+ if (rad > h / 2) rad = h / 2;
862
+
863
+ long nbytes = (long)w * h * 4;
864
+ VALUE str = rb_str_new(NULL, nbytes);
865
+ uint32_t *pixels = (uint32_t *)RSTRING_PTR(str);
866
+ memset(pixels, 0, nbytes);
867
+
868
+ const uint32_t fill_color = toast_argb(TOAST_FILL_A, TOAST_FILL_R, TOAST_FILL_G, TOAST_FILL_B);
869
+ const uint32_t border_color = toast_argb(TOAST_BDR_A, TOAST_BDR_R, TOAST_BDR_G, TOAST_BDR_B);
870
+
871
+ float border_w = 1.5f; /* border thickness in pixels */
872
+ float aa_w = 1.2f; /* anti-aliasing width */
873
+ float frad = (float)rad;
874
+
875
+ for (int py = 0; py < h; py++) {
876
+ for (int px = 0; px < w; px++) {
877
+ /* Signed distance from the rounded-rect boundary (negative = inside).
878
+ * SDF for a rounded rectangle per Inigo Quilez:
879
+ * https://iquilezles.org/articles/distfunctions/ */
880
+ float qx, qy;
881
+ float cx = (float)px + 0.5f;
882
+ float cy = (float)py + 0.5f;
883
+ float hw = (float)w * 0.5f;
884
+ float hh = (float)h * 0.5f;
885
+
886
+ /* Distance from center, reduced by half-size minus radius */
887
+ qx = fabsf(cx - hw) - (hw - frad);
888
+ qy = fabsf(cy - hh) - (hh - frad);
889
+
890
+ float dist;
891
+ float mx = qx > 0.0f ? qx : 0.0f;
892
+ float my = qy > 0.0f ? qy : 0.0f;
893
+ float outside = sqrtf(mx * mx + my * my);
894
+ float inside = qx > qy ? qx : qy;
895
+ if (inside < 0.0f) inside = 0.0f;
896
+ dist = outside + (outside > 0.0f ? 0.0f : (qx > qy ? qx : qy)) - frad;
897
+
898
+ uint32_t color;
899
+ if (dist >= aa_w * 0.5f) {
900
+ /* Outside: transparent */
901
+ color = 0;
902
+ } else if (dist >= -aa_w * 0.5f) {
903
+ /* Outer AA fringe: fade border from transparent to full.
904
+ * Non-premultiplied: RGB stays at border color, alpha varies. */
905
+ float t = 0.5f - dist / aa_w; /* 0..1 */
906
+ uint8_t a = (uint8_t)(TOAST_BDR_A * t + 0.5f);
907
+ if (a < 8) { color = 0; } /* suppress faint fringe dots */
908
+ else { color = toast_argb(a, TOAST_BDR_R, TOAST_BDR_G, TOAST_BDR_B); }
909
+ } else if (dist >= -(border_w - aa_w * 0.5f)) {
910
+ /* Solid border */
911
+ color = border_color;
912
+ } else if (dist >= -(border_w + aa_w * 0.5f)) {
913
+ /* Inner AA fringe: blend border → fill */
914
+ float t = (dist + border_w + aa_w * 0.5f) / aa_w; /* 1..0 inward */
915
+ uint8_t a = (uint8_t)(TOAST_BDR_A * t + TOAST_FILL_A * (1.0f - t) + 0.5f);
916
+ uint8_t r = (uint8_t)(TOAST_BDR_R * t + TOAST_FILL_R * (1.0f - t) + 0.5f);
917
+ uint8_t g = (uint8_t)(TOAST_BDR_G * t + TOAST_FILL_G * (1.0f - t) + 0.5f);
918
+ uint8_t b = (uint8_t)(TOAST_BDR_B * t + TOAST_FILL_B * (1.0f - t) + 0.5f);
919
+ color = toast_argb(a, r, g, b);
920
+ } else {
921
+ /* Fill interior */
922
+ color = fill_color;
923
+ }
924
+
925
+ pixels[py * w + px] = color;
926
+ }
927
+ }
928
+
929
+ return str;
930
+ }
931
+
932
+ /* --------------------------------------------------------- */
933
+ /* XOR delta for recording */
934
+ /* --------------------------------------------------------- */
935
+
936
+ /*
937
+ * Gemba.xor_delta(current, previous) → String
938
+ *
939
+ * XOR two equal-length binary strings byte-by-byte.
940
+ * Used for frame delta compression in recording.
941
+ */
942
+ static VALUE
943
+ mgba_xor_delta(VALUE mod, VALUE a, VALUE b)
944
+ {
945
+ (void)mod;
946
+ StringValue(a);
947
+ StringValue(b);
948
+
949
+ long len = RSTRING_LEN(a);
950
+ if (RSTRING_LEN(b) != len)
951
+ rb_raise(rb_eArgError, "strings must be the same length");
952
+
953
+ VALUE result = rb_str_new(NULL, len);
954
+ const unsigned char *sa = (const unsigned char *)RSTRING_PTR(a);
955
+ const unsigned char *sb = (const unsigned char *)RSTRING_PTR(b);
956
+ unsigned char *dst = (unsigned char *)RSTRING_PTR(result);
957
+
958
+ for (long i = 0; i < len; i++)
959
+ dst[i] = sa[i] ^ sb[i];
960
+
961
+ return result;
962
+ }
963
+
964
+ /*
965
+ * Gemba.count_changed_pixels(delta) → Integer
966
+ *
967
+ * Count the number of non-zero 4-byte pixels in a delta string.
968
+ * Used alongside xor_delta to measure per-frame change rates.
969
+ */
970
+ static VALUE
971
+ mgba_count_changed_pixels(VALUE mod, VALUE delta)
972
+ {
973
+ (void)mod;
974
+ StringValue(delta);
975
+
976
+ long len = RSTRING_LEN(delta);
977
+ const uint32_t *pixels = (const uint32_t *)RSTRING_PTR(delta);
978
+ long count = len / 4;
979
+ long changed = 0;
980
+
981
+ for (long i = 0; i < count; i++) {
982
+ if (pixels[i] != 0) changed++;
983
+ }
984
+
985
+ return LONG2NUM(changed);
986
+ }
987
+
988
+ /* --------------------------------------------------------- */
989
+ /* Init */
990
+ /* --------------------------------------------------------- */
991
+
992
+ void
993
+ Init_gemba_ext(void)
994
+ {
995
+ /* Install no-op logger before any mGBA calls */
996
+ mLogSetDefaultLogger(&s_null_logger);
997
+
998
+ /* Gemba module */
999
+ mGemba = rb_define_module("Gemba");
1000
+
1001
+ /* Gemba::Core class */
1002
+ cCore = rb_define_class_under(mGemba, "Core", rb_cObject);
1003
+ rb_define_alloc_func(cCore, mgba_core_alloc);
1004
+
1005
+ rb_define_method(cCore, "initialize", mgba_core_initialize, -1);
1006
+ rb_define_method(cCore, "run_frame", mgba_core_run_frame, 0);
1007
+ rb_define_method(cCore, "video_buffer", mgba_core_video_buffer, 0);
1008
+ rb_define_method(cCore, "video_buffer_argb", mgba_core_video_buffer_argb, 0);
1009
+ rb_define_method(cCore, "audio_buffer", mgba_core_audio_buffer, 0);
1010
+ rb_define_method(cCore, "set_keys", mgba_core_set_keys, 1);
1011
+ rb_define_method(cCore, "width", mgba_core_width, 0);
1012
+ rb_define_method(cCore, "height", mgba_core_height, 0);
1013
+ rb_define_method(cCore, "title", mgba_core_title, 0);
1014
+ rb_define_method(cCore, "game_code", mgba_core_game_code, 0);
1015
+ rb_define_method(cCore, "maker_code", mgba_core_maker_code, 0);
1016
+ rb_define_method(cCore, "checksum", mgba_core_checksum, 0);
1017
+ rb_define_method(cCore, "platform", mgba_core_platform, 0);
1018
+ rb_define_method(cCore, "rom_size", mgba_core_rom_size, 0);
1019
+ rb_define_method(cCore, "save_state_to_file", mgba_core_save_state_to_file, 1);
1020
+ rb_define_method(cCore, "load_state_from_file", mgba_core_load_state_from_file, 1);
1021
+ rb_define_method(cCore, "color_correction=", mgba_core_set_color_correction, 1);
1022
+ rb_define_method(cCore, "color_correction?", mgba_core_color_correction_p, 0);
1023
+ rb_define_method(cCore, "frame_blending=", mgba_core_set_frame_blending, 1);
1024
+ rb_define_method(cCore, "frame_blending?", mgba_core_frame_blending_p, 0);
1025
+ rb_define_method(cCore, "rewind_init", mgba_core_rewind_init, 1);
1026
+ rb_define_method(cCore, "rewind_deinit", mgba_core_rewind_deinit, 0);
1027
+ rb_define_method(cCore, "rewind_push", mgba_core_rewind_push, 0);
1028
+ rb_define_method(cCore, "rewind_pop", mgba_core_rewind_pop, 0);
1029
+ rb_define_method(cCore, "rewind_count", mgba_core_rewind_count, 0);
1030
+ rb_define_method(cCore, "destroy", mgba_core_destroy, 0);
1031
+ rb_define_method(cCore, "destroyed?", mgba_core_destroyed_p, 0);
1032
+
1033
+ /* GBA key constants (bitmask values for set_keys) */
1034
+ rb_define_const(mGemba, "KEY_A", INT2NUM(1 << GEMBA_KEY_A));
1035
+ rb_define_const(mGemba, "KEY_B", INT2NUM(1 << GEMBA_KEY_B));
1036
+ rb_define_const(mGemba, "KEY_SELECT", INT2NUM(1 << GEMBA_KEY_SELECT));
1037
+ rb_define_const(mGemba, "KEY_START", INT2NUM(1 << GEMBA_KEY_START));
1038
+ rb_define_const(mGemba, "KEY_RIGHT", INT2NUM(1 << GEMBA_KEY_RIGHT));
1039
+ rb_define_const(mGemba, "KEY_LEFT", INT2NUM(1 << GEMBA_KEY_LEFT));
1040
+ rb_define_const(mGemba, "KEY_UP", INT2NUM(1 << GEMBA_KEY_UP));
1041
+ rb_define_const(mGemba, "KEY_DOWN", INT2NUM(1 << GEMBA_KEY_DOWN));
1042
+ rb_define_const(mGemba, "KEY_R", INT2NUM(1 << GEMBA_KEY_R));
1043
+ rb_define_const(mGemba, "KEY_L", INT2NUM(1 << GEMBA_KEY_L));
1044
+
1045
+ /* Toast background generator */
1046
+ rb_define_module_function(mGemba, "toast_background", mgba_toast_background, 3);
1047
+
1048
+ /* XOR delta for recording */
1049
+ rb_define_module_function(mGemba, "xor_delta", mgba_xor_delta, 2);
1050
+ rb_define_module_function(mGemba, "count_changed_pixels", mgba_count_changed_pixels, 1);
1051
+ }