teek-sdl2 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42e232c86750e8313a051b00099dc677fa26888e8f0eace2a0fd2101b4d5aebd
4
- data.tar.gz: ae409b3dae88d5b89c3f5c4552fbd761ae627200c5c7a1ed6859682d5aa61197
3
+ metadata.gz: 723f31c93fcc52c30a208ef8ab42ed0d7ade64ac45bfda38cddad93cb18877c4
4
+ data.tar.gz: 1611a57531d9731651dec9391d93148a98a772106cbbcd044899c184d7cf65c4
5
5
  SHA512:
6
- metadata.gz: 89acada5767fe800d184cc8881053b4b76a0f75cb0742eb150d43567005a4f0474f8ccbad012bc991496e01cbf6c416519f1904c2451dbc866903aac9dad1c12
7
- data.tar.gz: dbbbb729914e2c17321889c958a4381547ba4944c7ab3f0c313ee8e91ae42695ebc077b0520d5dea4574fc583d9cb2d0dd30545c1c1d6a896ceab4223f561c72
6
+ metadata.gz: 6cdbf88dae339da4bbcd88181dc04292f7edc82c7f48a6256d29d3cc728803264eaba6a0e64984dd7031c90eaf545ff27c7a7857207c3dfcf0ef7cc95c8a9a64
7
+ data.tar.gz: 4f4176408fc19ecb389e02b478fac2028980965b3910ff93a326e75449f66a00ccbae6d600c3cf686a7a4c67595047a3a54534a6366110a8ca37bde033cf7b2f
@@ -128,6 +128,16 @@ unless pkg_config('SDL2_mixer') || have_library('SDL2_mixer', 'Mix_OpenAudio', '
128
128
  MSG
129
129
  end
130
130
 
131
- $srcs = ['teek_sdl2.c', 'sdl2surface.c', 'sdl2bridge.c', 'sdl2text.c', 'sdl2pixels.c', 'sdl2image.c', 'sdl2mixer.c', 'sdl2gamepad.c']
131
+ # SDL2_gfx for drawing primitives (circles, ellipses, polygons, etc.)
132
+ unless pkg_config('SDL2_gfx') || have_library('SDL2_gfx', 'filledCircleRGBA', 'SDL2/SDL2_gfxPrimitives.h')
133
+ abort <<~MSG
134
+ SDL2_gfx not found. Install it:
135
+ macOS: brew install sdl2_gfx
136
+ Debian: sudo apt-get install libsdl2-gfx-dev
137
+ Windows: pacman -S #{msys2_pkg_prefix}-SDL2_gfx (MSYS2)
138
+ MSG
139
+ end
140
+
141
+ $srcs = ['teek_sdl2.c', 'sdl2surface.c', 'sdl2bridge.c', 'sdl2text.c', 'sdl2pixels.c', 'sdl2image.c', 'sdl2mixer.c', 'sdl2audio.c', 'sdl2gamepad.c']
132
142
 
133
143
  create_makefile('teek_sdl2')
@@ -0,0 +1,433 @@
1
+ #include "teek_sdl2.h"
2
+
3
+ /* ---------------------------------------------------------
4
+ * SDL2 AudioStream — push-based real-time PCM audio output
5
+ *
6
+ * Wraps SDL_OpenAudioDevice + SDL_QueueAudio for streaming
7
+ * raw PCM data (emulators, synthesizers, procedural audio).
8
+ * Independent of SDL2_mixer — uses a separate audio device.
9
+ * --------------------------------------------------------- */
10
+
11
+ static VALUE cAudioStream;
12
+
13
+ static void
14
+ ensure_sdl_audio_init(void)
15
+ {
16
+ if (!(SDL_WasInit(SDL_INIT_AUDIO) & SDL_INIT_AUDIO)) {
17
+ if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
18
+ rb_raise(rb_eRuntimeError, "SDL_InitSubSystem(AUDIO) failed: %s",
19
+ SDL_GetError());
20
+ }
21
+ }
22
+ }
23
+
24
+ /* ---------------------------------------------------------
25
+ * AudioStream (wraps SDL_AudioDeviceID)
26
+ * --------------------------------------------------------- */
27
+
28
+ struct sdl2_audio_stream {
29
+ SDL_AudioDeviceID device_id;
30
+ int frequency;
31
+ int channels;
32
+ SDL_AudioFormat format;
33
+ int bytes_per_sample;
34
+ int destroyed;
35
+ };
36
+
37
+ static void
38
+ audio_stream_free(void *ptr)
39
+ {
40
+ struct sdl2_audio_stream *a = ptr;
41
+ if (!a->destroyed && a->device_id > 0) {
42
+ SDL_CloseAudioDevice(a->device_id);
43
+ a->device_id = 0;
44
+ a->destroyed = 1;
45
+ }
46
+ xfree(a);
47
+ }
48
+
49
+ static size_t
50
+ audio_stream_memsize(const void *ptr)
51
+ {
52
+ return sizeof(struct sdl2_audio_stream);
53
+ }
54
+
55
+ static const rb_data_type_t audio_stream_type = {
56
+ .wrap_struct_name = "TeekSDL2::AudioStream",
57
+ .function = {
58
+ .dmark = NULL,
59
+ .dfree = audio_stream_free,
60
+ .dsize = audio_stream_memsize,
61
+ },
62
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
63
+ };
64
+
65
+ static VALUE
66
+ audio_stream_alloc(VALUE klass)
67
+ {
68
+ struct sdl2_audio_stream *a;
69
+ VALUE obj = TypedData_Make_Struct(klass, struct sdl2_audio_stream,
70
+ &audio_stream_type, a);
71
+ a->device_id = 0;
72
+ a->frequency = 0;
73
+ a->channels = 0;
74
+ a->format = 0;
75
+ a->bytes_per_sample = 0;
76
+ a->destroyed = 0;
77
+ return obj;
78
+ }
79
+
80
+ static struct sdl2_audio_stream *
81
+ get_audio_stream(VALUE self)
82
+ {
83
+ struct sdl2_audio_stream *a;
84
+ TypedData_Get_Struct(self, struct sdl2_audio_stream, &audio_stream_type, a);
85
+ if (a->destroyed || a->device_id == 0) {
86
+ rb_raise(rb_eRuntimeError, "audio stream has been destroyed");
87
+ }
88
+ return a;
89
+ }
90
+
91
+ /* Helper: map Ruby symbol to SDL_AudioFormat + bytes_per_sample */
92
+ static int
93
+ resolve_format(VALUE sym, SDL_AudioFormat *out_fmt, int *out_bps)
94
+ {
95
+ ID id;
96
+ if (NIL_P(sym) || sym == Qundef) {
97
+ *out_fmt = AUDIO_S16SYS;
98
+ *out_bps = 2;
99
+ return 1;
100
+ }
101
+ if (!SYMBOL_P(sym)) return 0;
102
+
103
+ id = SYM2ID(sym);
104
+ if (id == rb_intern("s16")) {
105
+ *out_fmt = AUDIO_S16SYS;
106
+ *out_bps = 2;
107
+ } else if (id == rb_intern("f32")) {
108
+ *out_fmt = AUDIO_F32SYS;
109
+ *out_bps = 4;
110
+ } else if (id == rb_intern("u8")) {
111
+ *out_fmt = AUDIO_U8;
112
+ *out_bps = 1;
113
+ } else {
114
+ return 0;
115
+ }
116
+ return 1;
117
+ }
118
+
119
+ /*
120
+ * AudioStream.new(frequency: 44100, format: :s16, channels: 2)
121
+ *
122
+ * Opens a push-based audio output device. Starts paused —
123
+ * call #resume after queuing initial data.
124
+ */
125
+ static VALUE
126
+ audio_stream_initialize(int argc, VALUE *argv, VALUE self)
127
+ {
128
+ struct sdl2_audio_stream *a;
129
+ TypedData_Get_Struct(self, struct sdl2_audio_stream, &audio_stream_type, a);
130
+
131
+ ensure_sdl_audio_init();
132
+
133
+ /* Defaults */
134
+ int frequency = 44100;
135
+ int channels = 2;
136
+ SDL_AudioFormat format = AUDIO_S16SYS;
137
+ int bps = 2;
138
+
139
+ /* Parse keyword arguments */
140
+ VALUE kwargs;
141
+ rb_scan_args(argc, argv, ":", &kwargs);
142
+
143
+ if (!NIL_P(kwargs)) {
144
+ ID keys[3];
145
+ VALUE vals[3];
146
+ keys[0] = rb_intern("frequency");
147
+ keys[1] = rb_intern("format");
148
+ keys[2] = rb_intern("channels");
149
+
150
+ rb_get_kwargs(kwargs, keys, 0, 3, vals);
151
+
152
+ if (vals[0] != Qundef) {
153
+ frequency = NUM2INT(vals[0]);
154
+ if (frequency <= 0) {
155
+ rb_raise(rb_eArgError, "frequency must be positive");
156
+ }
157
+ }
158
+
159
+ if (vals[1] != Qundef) {
160
+ if (!resolve_format(vals[1], &format, &bps)) {
161
+ rb_raise(rb_eArgError,
162
+ "format must be :s16, :f32, or :u8");
163
+ }
164
+ }
165
+
166
+ if (vals[2] != Qundef) {
167
+ channels = NUM2INT(vals[2]);
168
+ if (channels < 1 || channels > 2) {
169
+ rb_raise(rb_eArgError, "channels must be 1 or 2");
170
+ }
171
+ }
172
+ }
173
+
174
+ /* Open audio device */
175
+ SDL_AudioSpec desired;
176
+ SDL_memset(&desired, 0, sizeof(desired));
177
+ desired.freq = frequency;
178
+ desired.format = format;
179
+ desired.channels = (Uint8)channels;
180
+ desired.samples = 2048;
181
+
182
+ SDL_AudioDeviceID dev = SDL_OpenAudioDevice(
183
+ NULL, 0, &desired, NULL, 0);
184
+ if (dev == 0) {
185
+ rb_raise(rb_eRuntimeError, "SDL_OpenAudioDevice failed: %s",
186
+ SDL_GetError());
187
+ }
188
+
189
+ a->device_id = dev;
190
+ a->frequency = frequency;
191
+ a->channels = channels;
192
+ a->format = format;
193
+ a->bytes_per_sample = bps;
194
+
195
+ return self;
196
+ }
197
+
198
+ /*
199
+ * stream.queue(data) -> nil
200
+ *
201
+ * Push raw PCM data to the audio device.
202
+ * +data+ must be a binary String matching the stream's format and channels.
203
+ */
204
+ static VALUE
205
+ audio_stream_queue(VALUE self, VALUE data)
206
+ {
207
+ struct sdl2_audio_stream *a = get_audio_stream(self);
208
+
209
+ StringValue(data);
210
+ if (RSTRING_LEN(data) == 0) return Qnil;
211
+
212
+ if (SDL_QueueAudio(a->device_id, RSTRING_PTR(data),
213
+ (Uint32)RSTRING_LEN(data)) < 0) {
214
+ rb_raise(rb_eRuntimeError, "SDL_QueueAudio failed: %s",
215
+ SDL_GetError());
216
+ }
217
+ return Qnil;
218
+ }
219
+
220
+ /*
221
+ * stream.queued_bytes -> Integer
222
+ *
223
+ * Bytes of audio data currently queued for playback.
224
+ */
225
+ static VALUE
226
+ audio_stream_queued_bytes(VALUE self)
227
+ {
228
+ struct sdl2_audio_stream *a = get_audio_stream(self);
229
+ Uint32 bytes = SDL_GetQueuedAudioSize(a->device_id);
230
+ return UINT2NUM(bytes);
231
+ }
232
+
233
+ /*
234
+ * stream.queued_samples -> Integer
235
+ *
236
+ * Number of audio samples (frames) currently queued.
237
+ * One sample = one value per channel.
238
+ */
239
+ static VALUE
240
+ audio_stream_queued_samples(VALUE self)
241
+ {
242
+ struct sdl2_audio_stream *a = get_audio_stream(self);
243
+ Uint32 bytes = SDL_GetQueuedAudioSize(a->device_id);
244
+ int frame_size = a->bytes_per_sample * a->channels;
245
+ return UINT2NUM(bytes / (Uint32)frame_size);
246
+ }
247
+
248
+ /*
249
+ * stream.resume -> nil
250
+ *
251
+ * Start or unpause audio playback.
252
+ */
253
+ static VALUE
254
+ audio_stream_resume(VALUE self)
255
+ {
256
+ struct sdl2_audio_stream *a = get_audio_stream(self);
257
+ SDL_PauseAudioDevice(a->device_id, 0);
258
+ return Qnil;
259
+ }
260
+
261
+ /*
262
+ * stream.pause -> nil
263
+ *
264
+ * Pause audio playback. Queued data is preserved.
265
+ */
266
+ static VALUE
267
+ audio_stream_pause(VALUE self)
268
+ {
269
+ struct sdl2_audio_stream *a = get_audio_stream(self);
270
+ SDL_PauseAudioDevice(a->device_id, 1);
271
+ return Qnil;
272
+ }
273
+
274
+ /*
275
+ * stream.playing? -> Boolean
276
+ *
277
+ * Whether the audio device is currently playing (not paused).
278
+ */
279
+ static VALUE
280
+ audio_stream_playing_p(VALUE self)
281
+ {
282
+ struct sdl2_audio_stream *a = get_audio_stream(self);
283
+ SDL_AudioStatus status = SDL_GetAudioDeviceStatus(a->device_id);
284
+ return status == SDL_AUDIO_PLAYING ? Qtrue : Qfalse;
285
+ }
286
+
287
+ /*
288
+ * stream.clear -> nil
289
+ *
290
+ * Flush all queued audio data.
291
+ */
292
+ static VALUE
293
+ audio_stream_clear(VALUE self)
294
+ {
295
+ struct sdl2_audio_stream *a = get_audio_stream(self);
296
+ SDL_ClearQueuedAudio(a->device_id);
297
+ return Qnil;
298
+ }
299
+
300
+ /*
301
+ * stream.frequency -> Integer
302
+ *
303
+ * Sample rate in Hz.
304
+ */
305
+ static VALUE
306
+ audio_stream_frequency(VALUE self)
307
+ {
308
+ struct sdl2_audio_stream *a = get_audio_stream(self);
309
+ return INT2NUM(a->frequency);
310
+ }
311
+
312
+ /*
313
+ * stream.channels -> Integer
314
+ *
315
+ * Number of audio channels (1 = mono, 2 = stereo).
316
+ */
317
+ static VALUE
318
+ audio_stream_channels(VALUE self)
319
+ {
320
+ struct sdl2_audio_stream *a = get_audio_stream(self);
321
+ return INT2NUM(a->channels);
322
+ }
323
+
324
+ /*
325
+ * stream.format -> Symbol
326
+ *
327
+ * Audio sample format (:s16, :f32, or :u8).
328
+ */
329
+ static VALUE
330
+ audio_stream_format(VALUE self)
331
+ {
332
+ struct sdl2_audio_stream *a = get_audio_stream(self);
333
+ if (a->format == AUDIO_S16SYS) return ID2SYM(rb_intern("s16"));
334
+ if (a->format == AUDIO_F32SYS) return ID2SYM(rb_intern("f32"));
335
+ if (a->format == AUDIO_U8) return ID2SYM(rb_intern("u8"));
336
+ return ID2SYM(rb_intern("unknown"));
337
+ }
338
+
339
+ /*
340
+ * stream.destroy -> nil
341
+ *
342
+ * Close the audio device. Further method calls will raise.
343
+ */
344
+ static VALUE
345
+ audio_stream_destroy(VALUE self)
346
+ {
347
+ struct sdl2_audio_stream *a;
348
+ TypedData_Get_Struct(self, struct sdl2_audio_stream, &audio_stream_type, a);
349
+ if (!a->destroyed && a->device_id > 0) {
350
+ SDL_CloseAudioDevice(a->device_id);
351
+ a->device_id = 0;
352
+ a->destroyed = 1;
353
+ }
354
+ return Qnil;
355
+ }
356
+
357
+ /*
358
+ * stream.destroyed? -> Boolean
359
+ *
360
+ * Whether the audio stream has been destroyed.
361
+ */
362
+ static VALUE
363
+ audio_stream_destroyed_p(VALUE self)
364
+ {
365
+ struct sdl2_audio_stream *a;
366
+ TypedData_Get_Struct(self, struct sdl2_audio_stream, &audio_stream_type, a);
367
+ return a->destroyed ? Qtrue : Qfalse;
368
+ }
369
+
370
+ /*
371
+ * AudioStream.available? -> Boolean
372
+ *
373
+ * Returns true if at least one audio output device is available.
374
+ * Use this to probe before opening a stream on headless/CI systems.
375
+ */
376
+ static VALUE
377
+ audio_stream_available_p(VALUE klass)
378
+ {
379
+ ensure_sdl_audio_init();
380
+ return SDL_GetNumAudioDevices(0) > 0 ? Qtrue : Qfalse;
381
+ }
382
+
383
+ /*
384
+ * AudioStream.device_count -> Integer
385
+ *
386
+ * Number of audio output devices detected by SDL2.
387
+ */
388
+ static VALUE
389
+ audio_stream_device_count(VALUE klass)
390
+ {
391
+ ensure_sdl_audio_init();
392
+ return INT2NUM(SDL_GetNumAudioDevices(0));
393
+ }
394
+
395
+ /*
396
+ * AudioStream.driver_name -> String or nil
397
+ *
398
+ * Name of the current SDL2 audio driver (e.g. "wasapi", "dummy"),
399
+ * or nil if audio is not initialized.
400
+ */
401
+ static VALUE
402
+ audio_stream_driver_name(VALUE klass)
403
+ {
404
+ const char *name = SDL_GetCurrentAudioDriver();
405
+ return name ? rb_str_new_cstr(name) : Qnil;
406
+ }
407
+
408
+ /* --------------------------------------------------------- */
409
+
410
+ void
411
+ Init_sdl2audio(VALUE mTeekSDL2)
412
+ {
413
+ cAudioStream = rb_define_class_under(mTeekSDL2, "AudioStream", rb_cObject);
414
+ rb_define_alloc_func(cAudioStream, audio_stream_alloc);
415
+
416
+ rb_define_method(cAudioStream, "initialize", audio_stream_initialize, -1);
417
+ rb_define_method(cAudioStream, "queue", audio_stream_queue, 1);
418
+ rb_define_method(cAudioStream, "queued_bytes", audio_stream_queued_bytes, 0);
419
+ rb_define_method(cAudioStream, "queued_samples", audio_stream_queued_samples, 0);
420
+ rb_define_method(cAudioStream, "resume", audio_stream_resume, 0);
421
+ rb_define_method(cAudioStream, "pause", audio_stream_pause, 0);
422
+ rb_define_method(cAudioStream, "playing?", audio_stream_playing_p, 0);
423
+ rb_define_method(cAudioStream, "clear", audio_stream_clear, 0);
424
+ rb_define_method(cAudioStream, "frequency", audio_stream_frequency, 0);
425
+ rb_define_method(cAudioStream, "channels", audio_stream_channels, 0);
426
+ rb_define_method(cAudioStream, "format", audio_stream_format, 0);
427
+ rb_define_method(cAudioStream, "destroy", audio_stream_destroy, 0);
428
+ rb_define_method(cAudioStream, "destroyed?", audio_stream_destroyed_p, 0);
429
+
430
+ rb_define_singleton_method(cAudioStream, "available?", audio_stream_available_p, 0);
431
+ rb_define_singleton_method(cAudioStream, "device_count", audio_stream_device_count, 0);
432
+ rb_define_singleton_method(cAudioStream, "driver_name", audio_stream_driver_name, 0);
433
+ }
@@ -9,18 +9,28 @@
9
9
  * --------------------------------------------------------- */
10
10
 
11
11
  /*
12
- * Teek::SDL2.create_renderer_from_handle(native_handle) -> Renderer
12
+ * Teek::SDL2.create_renderer_from_handle(native_handle, vsync=true) -> Renderer
13
13
  *
14
14
  * Creates an SDL2 window embedded in the native window identified by
15
15
  * native_handle (from Tk's 'winfo id'), then creates a GPU-accelerated
16
16
  * renderer on it.
17
17
  *
18
+ * When +vsync+ is true (the default), SDL_RENDERER_PRESENTVSYNC is set
19
+ * so SDL_RenderPresent blocks until the next display refresh. Pass false
20
+ * for applications that manage their own frame pacing (e.g. emulators
21
+ * syncing to an audio clock).
22
+ *
18
23
  * The Ruby Viewport class is responsible for getting the handle and
19
24
  * calling this. This C function just does the SDL2 work.
20
25
  */
21
26
  static VALUE
22
- bridge_create_renderer_from_handle(VALUE self, VALUE handle_val)
27
+ bridge_create_renderer_from_handle(int argc, VALUE *argv, VALUE self)
23
28
  {
29
+ VALUE handle_val, vsync_val;
30
+ rb_scan_args(argc, argv, "11", &handle_val, &vsync_val);
31
+
32
+ int use_vsync = NIL_P(vsync_val) ? 1 : RTEST(vsync_val);
33
+
24
34
  ensure_sdl2_init();
25
35
 
26
36
  /*
@@ -40,8 +50,10 @@ bridge_create_renderer_from_handle(VALUE self, VALUE handle_val)
40
50
  rb_raise(rb_eRuntimeError, "SDL_CreateWindowFrom failed: %s", SDL_GetError());
41
51
  }
42
52
 
43
- SDL_Renderer *sdl_ren = SDL_CreateRenderer(window, -1,
44
- SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
53
+ Uint32 flags = SDL_RENDERER_ACCELERATED;
54
+ if (use_vsync) flags |= SDL_RENDERER_PRESENTVSYNC;
55
+
56
+ SDL_Renderer *sdl_ren = SDL_CreateRenderer(window, -1, flags);
45
57
  if (!sdl_ren) {
46
58
  /* Fall back to software if GPU not available */
47
59
  sdl_ren = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
@@ -135,7 +147,7 @@ void
135
147
  Init_sdl2bridge(VALUE mTeekSDL2)
136
148
  {
137
149
  rb_define_module_function(mTeekSDL2, "create_renderer_from_handle",
138
- bridge_create_renderer_from_handle, 1);
150
+ bridge_create_renderer_from_handle, -1);
139
151
  rb_define_module_function(mTeekSDL2, "poll_events", bridge_poll_events, 0);
140
152
  rb_define_module_function(mTeekSDL2, "_event_check_fn_ptr", bridge_event_check_fn_ptr, 0);
141
153
  rb_define_module_function(mTeekSDL2, "sdl_quit", bridge_sdl_quit, 0);
@@ -33,6 +33,15 @@ ensure_gc_init(void)
33
33
  if (gc_subsystem_initialized) return;
34
34
 
35
35
  if (!(SDL_WasInit(SDL_INIT_GAMECONTROLLER) & SDL_INIT_GAMECONTROLLER)) {
36
+ /*
37
+ * By default SDL drops joystick/gamecontroller events when its
38
+ * window doesn't have focus. Since we embed SDL inside Tk, other
39
+ * Tk windows (e.g. Settings) can take focus while the user still
40
+ * needs gamepad input. Allow events regardless of focus.
41
+ * https://wiki.libsdl.org/SDL2/SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS
42
+ */
43
+ SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
44
+
36
45
  if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
37
46
  rb_raise(rb_eRuntimeError,
38
47
  "SDL_InitSubSystem(GAMECONTROLLER) failed: %s",
@@ -340,6 +349,24 @@ gamepad_name(VALUE self)
340
349
  return rb_str_new_cstr(name);
341
350
  }
342
351
 
352
+ /*
353
+ * Gamepad#guid -> String
354
+ *
355
+ * Returns the GUID string that identifies this controller's model/type.
356
+ * Same model controllers share the same GUID. Useful as a config key
357
+ * for persisting per-controller settings.
358
+ */
359
+ static VALUE
360
+ gamepad_guid(VALUE self)
361
+ {
362
+ struct sdl2_gamepad *gp = get_gamepad(self);
363
+ SDL_Joystick *joy = SDL_GameControllerGetJoystick(gp->controller);
364
+ SDL_JoystickGUID guid = SDL_JoystickGetGUID(joy);
365
+ char buf[33];
366
+ SDL_JoystickGetGUIDString(guid, buf, sizeof(buf));
367
+ return rb_str_new_cstr(buf);
368
+ }
369
+
343
370
  /*
344
371
  * Gamepad#attached? -> true or false
345
372
  *
@@ -544,6 +571,30 @@ gamepad_s_poll_events(VALUE klass)
544
571
  return INT2NUM(count);
545
572
  }
546
573
 
574
+ /*
575
+ * Gamepad.update_state -> nil
576
+ *
577
+ * Refreshes the internal state of all open game controllers
578
+ * WITHOUT pumping the platform event loop.
579
+ *
580
+ * SDL_PollEvent → SDL_PumpEvents → pumps the Cocoa run loop on macOS,
581
+ * which steals events from other UI toolkits (e.g. Tk).
582
+ * SDL_GameControllerUpdate → SDL_JoystickUpdate only (no event pump).
583
+ * See: https://github.com/libsdl-org/SDL/blob/SDL2/src/joystick/SDL_gamecontroller.c
584
+ *
585
+ * After calling this, Gamepad#button? and Gamepad#axis return
586
+ * updated values. Use this instead of poll_events when you only
587
+ * need fresh controller state and don't need event callbacks.
588
+ */
589
+ static VALUE
590
+ gamepad_s_update_state(VALUE klass)
591
+ {
592
+ if (gc_subsystem_initialized) {
593
+ SDL_GameControllerUpdate();
594
+ }
595
+ return Qnil;
596
+ }
597
+
547
598
  /* ---------------------------------------------------------
548
599
  * Callback registration
549
600
  * --------------------------------------------------------- */
@@ -838,6 +889,8 @@ Init_sdl2gamepad(VALUE mTeekSDL2)
838
889
  rb_define_singleton_method(cGamepad, "all", gamepad_s_all, 0);
839
890
  rb_define_singleton_method(cGamepad, "poll_events",
840
891
  gamepad_s_poll_events, 0);
892
+ rb_define_singleton_method(cGamepad, "update_state",
893
+ gamepad_s_update_state, 0);
841
894
  rb_define_singleton_method(cGamepad, "buttons", gamepad_s_buttons, 0);
842
895
  rb_define_singleton_method(cGamepad, "axes", gamepad_s_axes, 0);
843
896
 
@@ -857,6 +910,7 @@ Init_sdl2gamepad(VALUE mTeekSDL2)
857
910
 
858
911
  /* Instance methods */
859
912
  rb_define_method(cGamepad, "name", gamepad_name, 0);
913
+ rb_define_method(cGamepad, "guid", gamepad_guid, 0);
860
914
  rb_define_method(cGamepad, "attached?", gamepad_attached_p, 0);
861
915
  rb_define_method(cGamepad, "button?", gamepad_button_p, 1);
862
916
  rb_define_method(cGamepad, "axis", gamepad_axis, 1);