ruby2d 0.12.0 → 1.0.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 (619) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +7 -0
  3. data/README.md +141 -0
  4. data/USAGE.md +2435 -0
  5. data/assets/Rakefile +637 -0
  6. data/assets/build_support/CMakeLists.txt +94 -0
  7. data/assets/build_support/mruby/build_config.rb +34 -0
  8. data/assets/build_support/mruby/build_config_wasm.rb +30 -0
  9. data/assets/build_support/sdl_test.c +174 -0
  10. data/assets/deps.yaml +16 -0
  11. data/assets/platform/include/SDL3/SDL.h +91 -0
  12. data/assets/platform/include/SDL3/SDL_assert.h +705 -0
  13. data/assets/platform/include/SDL3/SDL_asyncio.h +550 -0
  14. data/assets/platform/include/SDL3/SDL_atomic.h +692 -0
  15. data/assets/platform/include/SDL3/SDL_audio.h +2370 -0
  16. data/assets/platform/include/SDL3/SDL_begin_code.h +553 -0
  17. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_bits.h +47 -27
  18. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_blendmode.h +59 -55
  19. data/assets/platform/include/SDL3/SDL_camera.h +535 -0
  20. data/assets/platform/include/SDL3/SDL_clipboard.h +331 -0
  21. data/assets/{include/SDL2/close_code.h → platform/include/SDL3/SDL_close_code.h} +9 -8
  22. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_copying.h +3 -1
  23. data/assets/platform/include/SDL3/SDL_cpuinfo.h +374 -0
  24. data/assets/platform/include/SDL3/SDL_dialog.h +343 -0
  25. data/assets/platform/include/SDL3/SDL_dlopennote.h +234 -0
  26. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_egl.h +11 -8
  27. data/assets/platform/include/SDL3/SDL_endian.h +645 -0
  28. data/assets/platform/include/SDL3/SDL_error.h +226 -0
  29. data/assets/platform/include/SDL3/SDL_events.h +1645 -0
  30. data/assets/platform/include/SDL3/SDL_filesystem.h +537 -0
  31. data/assets/platform/include/SDL3/SDL_gamepad.h +1656 -0
  32. data/assets/platform/include/SDL3/SDL_gpu.h +4608 -0
  33. data/assets/platform/include/SDL3/SDL_guid.h +106 -0
  34. data/assets/platform/include/SDL3/SDL_haptic.h +1461 -0
  35. data/assets/platform/include/SDL3/SDL_hidapi.h +571 -0
  36. data/assets/platform/include/SDL3/SDL_hints.h +5001 -0
  37. data/assets/platform/include/SDL3/SDL_init.h +507 -0
  38. data/assets/platform/include/SDL3/SDL_intrin.h +410 -0
  39. data/assets/platform/include/SDL3/SDL_iostream.h +1379 -0
  40. data/assets/platform/include/SDL3/SDL_joystick.h +1385 -0
  41. data/assets/platform/include/SDL3/SDL_keyboard.h +608 -0
  42. data/assets/platform/include/SDL3/SDL_keycode.h +347 -0
  43. data/assets/platform/include/SDL3/SDL_loadso.h +145 -0
  44. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_locale.h +40 -24
  45. data/assets/platform/include/SDL3/SDL_log.h +541 -0
  46. data/assets/platform/include/SDL3/SDL_main.h +704 -0
  47. data/assets/platform/include/SDL3/SDL_main_impl.h +151 -0
  48. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_messagebox.h +85 -48
  49. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_metal.h +29 -29
  50. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_misc.h +13 -12
  51. data/assets/platform/include/SDL3/SDL_mouse.h +813 -0
  52. data/assets/platform/include/SDL3/SDL_mutex.h +1117 -0
  53. data/assets/platform/include/SDL3/SDL_oldnames.h +1339 -0
  54. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl.h +1247 -278
  55. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengl_glext.h +5 -1
  56. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles.h +6 -7
  57. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2.h +10 -11
  58. data/assets/platform/include/SDL3/SDL_pen.h +198 -0
  59. data/assets/platform/include/SDL3/SDL_pixels.h +1441 -0
  60. data/assets/{include/SDL2/SDL_test_log.h → platform/include/SDL3/SDL_platform.h} +25 -26
  61. data/assets/platform/include/SDL3/SDL_platform_defines.h +497 -0
  62. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_power.h +40 -20
  63. data/assets/platform/include/SDL3/SDL_process.h +441 -0
  64. data/assets/platform/include/SDL3/SDL_properties.h +572 -0
  65. data/assets/platform/include/SDL3/SDL_rect.h +528 -0
  66. data/assets/platform/include/SDL3/SDL_render.h +3028 -0
  67. data/assets/platform/include/SDL3/SDL_revision.h +59 -0
  68. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_scancode.h +59 -68
  69. data/assets/platform/include/SDL3/SDL_sensor.h +321 -0
  70. data/assets/platform/include/SDL3/SDL_stdinc.h +6186 -0
  71. data/assets/platform/include/SDL3/SDL_storage.h +686 -0
  72. data/assets/platform/include/SDL3/SDL_surface.h +1769 -0
  73. data/assets/platform/include/SDL3/SDL_system.h +840 -0
  74. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test.h +16 -22
  75. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_assert.h +27 -34
  76. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_common.h +109 -52
  77. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_compare.h +22 -14
  78. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_crc32.h +27 -30
  79. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_font.h +50 -49
  80. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_fuzzer.h +64 -79
  81. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_harness.h +47 -30
  82. data/assets/platform/include/SDL3/SDL_test_log.h +83 -0
  83. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_md5.h +21 -28
  84. data/assets/{include/SDL2 → platform/include/SDL3}/SDL_test_memory.h +19 -16
  85. data/assets/platform/include/SDL3/SDL_thread.h +602 -0
  86. data/assets/platform/include/SDL3/SDL_time.h +249 -0
  87. data/assets/platform/include/SDL3/SDL_timer.h +454 -0
  88. data/assets/platform/include/SDL3/SDL_touch.h +184 -0
  89. data/assets/platform/include/SDL3/SDL_tray.h +544 -0
  90. data/assets/platform/include/SDL3/SDL_version.h +198 -0
  91. data/assets/platform/include/SDL3/SDL_video.h +3497 -0
  92. data/assets/platform/include/SDL3/SDL_vulkan.h +287 -0
  93. data/assets/platform/include/SDL3_image/SDL_image.h +3328 -0
  94. data/assets/platform/include/SDL3_mixer/SDL_mixer.h +3369 -0
  95. data/assets/platform/include/SDL3_ttf/SDL_textengine.h +181 -0
  96. data/assets/platform/include/SDL3_ttf/SDL_ttf.h +2833 -0
  97. data/assets/{include → platform/include}/mrbconf.h +42 -31
  98. data/assets/{include → platform/include}/mruby/array.h +3 -4
  99. data/assets/{include → platform/include}/mruby/boxing_nan.h +10 -0
  100. data/assets/platform/include/mruby/boxing_no.h +59 -0
  101. data/assets/{include → platform/include}/mruby/boxing_word.h +72 -26
  102. data/assets/platform/include/mruby/class.h +157 -0
  103. data/assets/{include → platform/include}/mruby/common.h +30 -1
  104. data/assets/{include → platform/include}/mruby/compile.h +52 -74
  105. data/assets/{include → platform/include}/mruby/data.h +2 -2
  106. data/assets/{include → platform/include}/mruby/debug.h +12 -5
  107. data/assets/{include → platform/include}/mruby/dump.h +7 -9
  108. data/assets/{include → platform/include}/mruby/endian.h +1 -1
  109. data/assets/platform/include/mruby/error.h +173 -0
  110. data/assets/platform/include/mruby/gc.h +79 -0
  111. data/assets/{include → platform/include}/mruby/hash.h +1 -7
  112. data/assets/platform/include/mruby/internal.h +282 -0
  113. data/assets/platform/include/mruby/irep.h +143 -0
  114. data/assets/{include → platform/include}/mruby/istruct.h +1 -1
  115. data/assets/platform/include/mruby/khash.h +455 -0
  116. data/assets/platform/include/mruby/mempool.h +19 -0
  117. data/assets/{include → platform/include}/mruby/numeric.h +7 -10
  118. data/assets/platform/include/mruby/object.h +51 -0
  119. data/assets/{include → platform/include}/mruby/opcode.h +5 -5
  120. data/assets/{include → platform/include}/mruby/ops.h +19 -6
  121. data/assets/platform/include/mruby/presym/id.h +1447 -0
  122. data/assets/{include → platform/include}/mruby/presym/scanning.h +10 -7
  123. data/assets/platform/include/mruby/presym/table.h +2893 -0
  124. data/assets/platform/include/mruby/presym.h +65 -0
  125. data/assets/platform/include/mruby/proc.h +184 -0
  126. data/assets/{include → platform/include}/mruby/range.h +0 -2
  127. data/assets/{include → platform/include}/mruby/string.h +33 -53
  128. data/assets/platform/include/mruby/throw.h +81 -0
  129. data/assets/{include → platform/include}/mruby/value.h +64 -29
  130. data/assets/{include → platform/include}/mruby/variable.h +6 -23
  131. data/assets/{include → platform/include}/mruby/version.h +7 -7
  132. data/assets/{include → platform/include}/mruby.h +238 -116
  133. data/assets/platform/macos-arm64/bin/mrbc +0 -0
  134. data/assets/platform/macos-arm64/lib/libSDL3.a +0 -0
  135. data/assets/platform/macos-arm64/lib/libSDL3_image.a +0 -0
  136. data/assets/platform/macos-arm64/lib/libSDL3_mixer.a +0 -0
  137. data/assets/platform/macos-arm64/lib/libSDL3_ttf.a +0 -0
  138. data/assets/platform/macos-arm64/lib/libmruby.a +0 -0
  139. data/assets/platform/wasm/lib/libFLAC.a +0 -0
  140. data/assets/platform/wasm/lib/libSDL3.a +0 -0
  141. data/assets/platform/wasm/lib/libSDL3_image.a +0 -0
  142. data/assets/platform/wasm/lib/libSDL3_mixer.a +0 -0
  143. data/assets/platform/wasm/lib/libSDL3_ttf.a +0 -0
  144. data/assets/platform/wasm/lib/libfreetype.a +0 -0
  145. data/assets/platform/wasm/lib/libharfbuzz.a +0 -0
  146. data/assets/platform/wasm/lib/libmruby.a +0 -0
  147. data/assets/platform/wasm/lib/libogg.a +0 -0
  148. data/assets/platform/wasm/lib/libplutosvg.a +0 -0
  149. data/assets/platform/wasm/lib/libplutovg.a +0 -0
  150. data/assets/platform/wasm/lib/libpng.a +0 -0
  151. data/assets/platform/wasm/lib/libpng16.a +0 -0
  152. data/assets/platform/wasm/lib/libvorbis.a +0 -0
  153. data/assets/platform/wasm/lib/libvorbisfile.a +0 -0
  154. data/assets/platform/wasm/lib/libzlibstatic.a +0 -0
  155. data/assets/platform/windows-arm64-mingw-ucrt/bin/mrbc.exe +0 -0
  156. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3.a +0 -0
  157. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_image.a +0 -0
  158. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
  159. data/assets/platform/windows-arm64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
  160. data/assets/platform/windows-arm64-mingw-ucrt/lib/libmruby.a +0 -0
  161. data/assets/platform/windows-x86_64-mingw-ucrt/bin/mrbc.exe +0 -0
  162. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3.a +0 -0
  163. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_image.a +0 -0
  164. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_mixer.a +0 -0
  165. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libSDL3_ttf.a +0 -0
  166. data/assets/platform/windows-x86_64-mingw-ucrt/lib/libmruby.a +0 -0
  167. data/assets/resources/fonts/outfit/OFL.txt +93 -0
  168. data/assets/resources/fonts/outfit/outfit.ttf +0 -0
  169. data/assets/resources/fonts/roboto_mono/OFL.txt +91 -0
  170. data/assets/resources/fonts/roboto_mono/roboto_mono.ttf +0 -0
  171. data/assets/resources/icons/icon.icns +0 -0
  172. data/assets/resources/icons/icon.png +0 -0
  173. data/assets/resources/spritesheets/License.txt +28 -0
  174. data/assets/resources/spritesheets/spritesheet-backgrounds.png +0 -0
  175. data/assets/resources/spritesheets/spritesheet-backgrounds.xml +16 -0
  176. data/assets/resources/spritesheets/spritesheet-characters.png +0 -0
  177. data/assets/resources/spritesheets/spritesheet-characters.xml +47 -0
  178. data/assets/resources/spritesheets/spritesheet-enemies.png +0 -0
  179. data/assets/resources/spritesheets/spritesheet-enemies.xml +62 -0
  180. data/assets/resources/spritesheets/spritesheet-tiles.png +0 -0
  181. data/assets/resources/spritesheets/spritesheet-tiles.xml +316 -0
  182. data/assets/{wasm → resources/web}/template.html +1 -1
  183. data/assets/target.rb +131 -0
  184. data/bin/ruby2d +281 -82
  185. data/examples/astar.rb +269 -0
  186. data/examples/asteroids.rb +403 -0
  187. data/examples/bezier_editor.rb +96 -0
  188. data/examples/boids.rb +191 -0
  189. data/examples/bouncing_balls.rb +119 -0
  190. data/examples/breakout.rb +186 -0
  191. data/examples/constellations.rb +100 -0
  192. data/examples/dashboard.rb +133 -0
  193. data/examples/double_pendulum.rb +254 -0
  194. data/examples/falling_sand.rb +291 -0
  195. data/examples/fireworks.rb +110 -0
  196. data/examples/fluid_dynamics.rb +340 -0
  197. data/examples/fourier_epicycles.rb +142 -0
  198. data/examples/fractal_tree.rb +326 -0
  199. data/examples/game_of_life.rb +148 -0
  200. data/examples/gamepads.rb +333 -0
  201. data/examples/hill_driver.rb +613 -0
  202. data/examples/logo_animation.rb +251 -0
  203. data/examples/lorenz_attractor.rb +115 -0
  204. data/examples/mandelbrot.rb +207 -0
  205. data/examples/marching_squares.rb +200 -0
  206. data/examples/maze.rb +323 -0
  207. data/examples/nbody.rb +338 -0
  208. data/examples/outrun.rb +391 -0
  209. data/examples/pixel_paint.rb +313 -0
  210. data/examples/platformer.rb +335 -0
  211. data/examples/ray_casting_maze.rb +175 -0
  212. data/examples/shadow_casting.rb +151 -0
  213. data/examples/shape_recognizer.rb +908 -0
  214. data/examples/snake.rb +144 -0
  215. data/examples/sprite_sheets.rb +246 -0
  216. data/examples/swarm.rb +206 -0
  217. data/examples/tsp_annealing.rb +104 -0
  218. data/examples/ui_controls.rb +163 -0
  219. data/examples/wireframe_3d.rb +139 -0
  220. data/ext/ruby2d/audio.c +324 -0
  221. data/ext/ruby2d/canvas.c +1715 -494
  222. data/ext/ruby2d/ext.c +541 -0
  223. data/ext/ruby2d/extconf.rb +122 -145
  224. data/ext/ruby2d/font.c +566 -20
  225. data/ext/ruby2d/fps.c +92 -0
  226. data/ext/ruby2d/image.c +466 -41
  227. data/ext/ruby2d/ruby2d.c +374 -1890
  228. data/ext/ruby2d/ruby2d.h +840 -460
  229. data/ext/ruby2d/shapes.c +931 -101
  230. data/ext/ruby2d/text.c +586 -21
  231. data/ext/ruby2d/window.c +1693 -283
  232. data/lib/ruby2d/atlas_parser.rb +194 -0
  233. data/lib/ruby2d/audio.rb +73 -0
  234. data/lib/ruby2d/benchmark.rb +259 -0
  235. data/lib/ruby2d/bitmap_text.rb +155 -0
  236. data/lib/ruby2d/button.rb +402 -0
  237. data/lib/ruby2d/canvas.rb +703 -279
  238. data/lib/ruby2d/circle.rb +206 -29
  239. data/lib/ruby2d/cli/browser.rb +314 -0
  240. data/lib/ruby2d/cli/build.rb +471 -257
  241. data/lib/ruby2d/cli/colorize.rb +7 -5
  242. data/lib/ruby2d/cli/examples.rb +127 -0
  243. data/lib/ruby2d/cli/launch.rb +24 -39
  244. data/lib/ruby2d/cli/lib_files.rb +48 -0
  245. data/lib/ruby2d/cli/messages.rb +40 -0
  246. data/lib/ruby2d/cli/setup.rb +273 -0
  247. data/lib/ruby2d/cli/static_server.rb +160 -0
  248. data/lib/ruby2d/cli/usage.rb +255 -0
  249. data/lib/ruby2d/color.rb +191 -69
  250. data/lib/ruby2d/core.rb +37 -19
  251. data/lib/ruby2d/deps_help.rb +89 -0
  252. data/lib/ruby2d/dsl.rb +51 -12
  253. data/lib/ruby2d/ellipse.rb +228 -0
  254. data/lib/ruby2d/exceptions.rb +0 -2
  255. data/lib/ruby2d/font.rb +34 -74
  256. data/lib/ruby2d/gamepad.rb +323 -0
  257. data/lib/ruby2d/gem_paths.rb +19 -0
  258. data/lib/ruby2d/image.rb +155 -52
  259. data/lib/ruby2d/interactive.rb +103 -0
  260. data/lib/ruby2d/json_parser.rb +268 -0
  261. data/lib/ruby2d/line.rb +194 -85
  262. data/lib/ruby2d/mruby_compat.rb +12 -0
  263. data/lib/ruby2d/polygon.rb +297 -0
  264. data/lib/ruby2d/polyline.rb +368 -0
  265. data/lib/ruby2d/quad.rb +358 -94
  266. data/lib/ruby2d/rectangle.rb +77 -19
  267. data/lib/ruby2d/renderable.rb +411 -11
  268. data/lib/ruby2d/sprite.rb +471 -180
  269. data/lib/ruby2d/sprite_sheet.rb +64 -0
  270. data/lib/ruby2d/square.rb +29 -17
  271. data/lib/ruby2d/text.rb +196 -65
  272. data/lib/ruby2d/tileset.rb +139 -81
  273. data/lib/ruby2d/triangle.rb +324 -89
  274. data/lib/ruby2d/version.rb +1 -3
  275. data/lib/ruby2d/vertices.rb +62 -85
  276. data/lib/ruby2d/warnings.rb +29 -0
  277. data/lib/ruby2d/window/class_methods.rb +218 -0
  278. data/lib/ruby2d/window/gamepad_events.rb +141 -0
  279. data/lib/ruby2d/window/key_events.rb +74 -0
  280. data/lib/ruby2d/window/mouse_events.rb +171 -0
  281. data/lib/ruby2d/window/object_events.rb +168 -0
  282. data/lib/ruby2d/window.rb +632 -585
  283. data/lib/ruby2d.rb +14 -19
  284. metadata +275 -428
  285. data/assets/app.icns +0 -0
  286. data/assets/include/GLES2/gl2.h +0 -656
  287. data/assets/include/GLES2/gl2ext.h +0 -3949
  288. data/assets/include/GLES2/gl2ext_angle.h +0 -701
  289. data/assets/include/GLES2/gl2platform.h +0 -27
  290. data/assets/include/GLES3/gl3.h +0 -1192
  291. data/assets/include/GLES3/gl31.h +0 -1507
  292. data/assets/include/GLES3/gl32.h +0 -1808
  293. data/assets/include/GLES3/gl3platform.h +0 -27
  294. data/assets/include/KHR/khrplatform.h +0 -290
  295. data/assets/include/SDL2/SDL.h +0 -233
  296. data/assets/include/SDL2/SDL_assert.h +0 -326
  297. data/assets/include/SDL2/SDL_atomic.h +0 -415
  298. data/assets/include/SDL2/SDL_audio.h +0 -1500
  299. data/assets/include/SDL2/SDL_clipboard.h +0 -141
  300. data/assets/include/SDL2/SDL_config.h +0 -61
  301. data/assets/include/SDL2/SDL_config_android.h +0 -194
  302. data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
  303. data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
  304. data/assets/include/SDL2/SDL_config_macosx.h +0 -277
  305. data/assets/include/SDL2/SDL_config_minimal.h +0 -95
  306. data/assets/include/SDL2/SDL_config_ngage.h +0 -89
  307. data/assets/include/SDL2/SDL_config_os2.h +0 -207
  308. data/assets/include/SDL2/SDL_config_pandora.h +0 -141
  309. data/assets/include/SDL2/SDL_config_windows.h +0 -331
  310. data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
  311. data/assets/include/SDL2/SDL_config_winrt.h +0 -220
  312. data/assets/include/SDL2/SDL_config_xbox.h +0 -235
  313. data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
  314. data/assets/include/SDL2/SDL_endian.h +0 -348
  315. data/assets/include/SDL2/SDL_error.h +0 -163
  316. data/assets/include/SDL2/SDL_events.h +0 -1166
  317. data/assets/include/SDL2/SDL_filesystem.h +0 -149
  318. data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
  319. data/assets/include/SDL2/SDL_gesture.h +0 -117
  320. data/assets/include/SDL2/SDL_guid.h +0 -100
  321. data/assets/include/SDL2/SDL_haptic.h +0 -1341
  322. data/assets/include/SDL2/SDL_hidapi.h +0 -451
  323. data/assets/include/SDL2/SDL_hints.h +0 -2569
  324. data/assets/include/SDL2/SDL_image.h +0 -2173
  325. data/assets/include/SDL2/SDL_joystick.h +0 -1066
  326. data/assets/include/SDL2/SDL_keyboard.h +0 -353
  327. data/assets/include/SDL2/SDL_keycode.h +0 -358
  328. data/assets/include/SDL2/SDL_loadso.h +0 -115
  329. data/assets/include/SDL2/SDL_log.h +0 -404
  330. data/assets/include/SDL2/SDL_main.h +0 -275
  331. data/assets/include/SDL2/SDL_mixer.h +0 -2784
  332. data/assets/include/SDL2/SDL_mouse.h +0 -465
  333. data/assets/include/SDL2/SDL_mutex.h +0 -471
  334. data/assets/include/SDL2/SDL_name.h +0 -33
  335. data/assets/include/SDL2/SDL_pixels.h +0 -644
  336. data/assets/include/SDL2/SDL_platform.h +0 -261
  337. data/assets/include/SDL2/SDL_quit.h +0 -58
  338. data/assets/include/SDL2/SDL_rect.h +0 -376
  339. data/assets/include/SDL2/SDL_render.h +0 -1919
  340. data/assets/include/SDL2/SDL_revision.h +0 -6
  341. data/assets/include/SDL2/SDL_rwops.h +0 -841
  342. data/assets/include/SDL2/SDL_sensor.h +0 -322
  343. data/assets/include/SDL2/SDL_shape.h +0 -155
  344. data/assets/include/SDL2/SDL_stdinc.h +0 -830
  345. data/assets/include/SDL2/SDL_surface.h +0 -997
  346. data/assets/include/SDL2/SDL_system.h +0 -623
  347. data/assets/include/SDL2/SDL_syswm.h +0 -386
  348. data/assets/include/SDL2/SDL_test_images.h +0 -78
  349. data/assets/include/SDL2/SDL_test_random.h +0 -115
  350. data/assets/include/SDL2/SDL_thread.h +0 -464
  351. data/assets/include/SDL2/SDL_timer.h +0 -222
  352. data/assets/include/SDL2/SDL_touch.h +0 -150
  353. data/assets/include/SDL2/SDL_ttf.h +0 -2316
  354. data/assets/include/SDL2/SDL_types.h +0 -29
  355. data/assets/include/SDL2/SDL_version.h +0 -204
  356. data/assets/include/SDL2/SDL_video.h +0 -2150
  357. data/assets/include/SDL2/SDL_vulkan.h +0 -215
  358. data/assets/include/SDL2/begin_code.h +0 -187
  359. data/assets/include/mruby/boxing_no.h +0 -58
  360. data/assets/include/mruby/class.h +0 -108
  361. data/assets/include/mruby/error.h +0 -147
  362. data/assets/include/mruby/gc.h +0 -94
  363. data/assets/include/mruby/irep.h +0 -147
  364. data/assets/include/mruby/khash.h +0 -284
  365. data/assets/include/mruby/object.h +0 -43
  366. data/assets/include/mruby/presym/disable.h +0 -70
  367. data/assets/include/mruby/presym/enable.h +0 -37
  368. data/assets/include/mruby/presym.h +0 -40
  369. data/assets/include/mruby/proc.h +0 -211
  370. data/assets/include/mruby/throw.h +0 -66
  371. data/assets/macos/universal/bin/mrbc +0 -0
  372. data/assets/macos/universal/lib/libFLAC.a +0 -0
  373. data/assets/macos/universal/lib/libSDL2.a +0 -0
  374. data/assets/macos/universal/lib/libSDL2_image.a +0 -0
  375. data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
  376. data/assets/macos/universal/lib/libSDL2_ttf.a +0 -0
  377. data/assets/macos/universal/lib/libavif.a +0 -0
  378. data/assets/macos/universal/lib/libbrotlicommon-static.a +0 -0
  379. data/assets/macos/universal/lib/libbrotlidec-static.a +0 -0
  380. data/assets/macos/universal/lib/libfreetype.a +0 -0
  381. data/assets/macos/universal/lib/libgraphite2.a +0 -0
  382. data/assets/macos/universal/lib/libharfbuzz.a +0 -0
  383. data/assets/macos/universal/lib/libhwy.a +0 -0
  384. data/assets/macos/universal/lib/libjpeg.a +0 -0
  385. data/assets/macos/universal/lib/libjxl.a +0 -0
  386. data/assets/macos/universal/lib/libmodplug.a +0 -0
  387. data/assets/macos/universal/lib/libmpg123.a +0 -0
  388. data/assets/macos/universal/lib/libmruby.a +0 -0
  389. data/assets/macos/universal/lib/libogg.a +0 -0
  390. data/assets/macos/universal/lib/libpng.a +0 -0
  391. data/assets/macos/universal/lib/libtiff.a +0 -0
  392. data/assets/macos/universal/lib/libvorbis.a +0 -0
  393. data/assets/macos/universal/lib/libvorbisfile.a +0 -0
  394. data/assets/macos/universal/lib/libwebp.a +0 -0
  395. data/assets/macos/universal/lib/libzstd.a +0 -0
  396. data/assets/test_media/README.md +0 -3
  397. data/assets/test_media/bitstream_vera/COPYRIGHT.txt +0 -124
  398. data/assets/test_media/bitstream_vera/vera.ttf +0 -0
  399. data/assets/test_media/boom.png +0 -0
  400. data/assets/test_media/coin.png +0 -0
  401. data/assets/test_media/colors.png +0 -0
  402. data/assets/test_media/controller.png +0 -0
  403. data/assets/test_media/dial.wav +0 -0
  404. data/assets/test_media/hero.png +0 -0
  405. data/assets/test_media/image.bmp +0 -0
  406. data/assets/test_media/image.jpg +0 -0
  407. data/assets/test_media/image.png +0 -0
  408. data/assets/test_media/music.flac +0 -0
  409. data/assets/test_media/music.mp3 +0 -0
  410. data/assets/test_media/music.ogg +0 -0
  411. data/assets/test_media/music.wav +0 -0
  412. data/assets/test_media/originals/boom.pxm +0 -0
  413. data/assets/test_media/originals/coin.pxm +0 -0
  414. data/assets/test_media/originals/controller.sketch +0 -0
  415. data/assets/test_media/originals/hero.pxm +0 -0
  416. data/assets/test_media/originals/image.pxm +0 -0
  417. data/assets/test_media/originals/music.caf +0 -0
  418. data/assets/test_media/originals/texture_atlas.pxm +0 -0
  419. data/assets/test_media/rondo_alla_turka.ogg +0 -0
  420. data/assets/test_media/sound.flac +0 -0
  421. data/assets/test_media/sound.mp3 +0 -0
  422. data/assets/test_media/sound.ogg +0 -0
  423. data/assets/test_media/sound.wav +0 -0
  424. data/assets/test_media/sprite_sheet.png +0 -0
  425. data/assets/test_media/texture_atlas.png +0 -0
  426. data/assets/wasm/libmruby.a +0 -0
  427. data/assets/windows/mingw-w64-ucrt-x86_64/bin/mrbc.exe +0 -0
  428. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
  429. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libLerc.a +0 -0
  430. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
  431. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_image.a +0 -0
  432. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
  433. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_ttf.a +0 -0
  434. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlicommon.a +0 -0
  435. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbrotlidec.a +0 -0
  436. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libbz2.a +0 -0
  437. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libdeflate.a +0 -0
  438. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libfreetype.a +0 -0
  439. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libglew32.a +0 -0
  440. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libgraphite2.a +0 -0
  441. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libharfbuzz.a +0 -0
  442. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libhwy.a +0 -0
  443. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjbig.a +0 -0
  444. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjpeg.a +0 -0
  445. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libjxl.a +0 -0
  446. data/assets/windows/mingw-w64-ucrt-x86_64/lib/liblzma.a +0 -0
  447. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
  448. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
  449. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmruby.a +0 -0
  450. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
  451. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
  452. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
  453. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libpng.a +0 -0
  454. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
  455. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libssp.a +0 -1
  456. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
  457. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libtiff.a +0 -0
  458. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
  459. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
  460. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libwebp.a +0 -0
  461. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
  462. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libzstd.a +0 -0
  463. data/assets/windows/mingw-w64-x86_64/bin/mrbc.exe +0 -0
  464. data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
  465. data/assets/windows/mingw-w64-x86_64/lib/libLerc.a +0 -0
  466. data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
  467. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_image.a +0 -0
  468. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
  469. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_ttf.a +0 -0
  470. data/assets/windows/mingw-w64-x86_64/lib/libbrotlicommon.a +0 -0
  471. data/assets/windows/mingw-w64-x86_64/lib/libbrotlidec.a +0 -0
  472. data/assets/windows/mingw-w64-x86_64/lib/libbz2.a +0 -0
  473. data/assets/windows/mingw-w64-x86_64/lib/libdeflate.a +0 -0
  474. data/assets/windows/mingw-w64-x86_64/lib/libfreetype.a +0 -0
  475. data/assets/windows/mingw-w64-x86_64/lib/libglew32.a +0 -0
  476. data/assets/windows/mingw-w64-x86_64/lib/libgraphite2.a +0 -0
  477. data/assets/windows/mingw-w64-x86_64/lib/libharfbuzz.a +0 -0
  478. data/assets/windows/mingw-w64-x86_64/lib/libhwy.a +0 -0
  479. data/assets/windows/mingw-w64-x86_64/lib/libjbig.a +0 -0
  480. data/assets/windows/mingw-w64-x86_64/lib/libjpeg.a +0 -0
  481. data/assets/windows/mingw-w64-x86_64/lib/libjxl.a +0 -0
  482. data/assets/windows/mingw-w64-x86_64/lib/liblzma.a +0 -0
  483. data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
  484. data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
  485. data/assets/windows/mingw-w64-x86_64/lib/libmruby.a +0 -0
  486. data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
  487. data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
  488. data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
  489. data/assets/windows/mingw-w64-x86_64/lib/libpng.a +0 -0
  490. data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
  491. data/assets/windows/mingw-w64-x86_64/lib/libssp.a +0 -1
  492. data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
  493. data/assets/windows/mingw-w64-x86_64/lib/libtiff.a +0 -0
  494. data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
  495. data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
  496. data/assets/windows/mingw-w64-x86_64/lib/libwebp.a +0 -0
  497. data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
  498. data/assets/windows/mingw-w64-x86_64/lib/libzstd.a +0 -0
  499. data/assets/xcode/ios/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
  500. data/assets/xcode/ios/Assets.xcassets/Contents.json +0 -6
  501. data/assets/xcode/ios/Base.lproj/LaunchScreen.storyboard +0 -27
  502. data/assets/xcode/ios/Info.plist +0 -43
  503. data/assets/xcode/ios/MRuby.framework/Headers/mrbconf.h +0 -143
  504. data/assets/xcode/ios/MRuby.framework/Headers/mruby/array.h +0 -280
  505. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
  506. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
  507. data/assets/xcode/ios/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
  508. data/assets/xcode/ios/MRuby.framework/Headers/mruby/class.h +0 -97
  509. data/assets/xcode/ios/MRuby.framework/Headers/mruby/common.h +0 -77
  510. data/assets/xcode/ios/MRuby.framework/Headers/mruby/compile.h +0 -195
  511. data/assets/xcode/ios/MRuby.framework/Headers/mruby/data.h +0 -75
  512. data/assets/xcode/ios/MRuby.framework/Headers/mruby/debug.h +0 -67
  513. data/assets/xcode/ios/MRuby.framework/Headers/mruby/dump.h +0 -196
  514. data/assets/xcode/ios/MRuby.framework/Headers/mruby/error.h +0 -75
  515. data/assets/xcode/ios/MRuby.framework/Headers/mruby/gc.h +0 -91
  516. data/assets/xcode/ios/MRuby.framework/Headers/mruby/hash.h +0 -215
  517. data/assets/xcode/ios/MRuby.framework/Headers/mruby/irep.h +0 -75
  518. data/assets/xcode/ios/MRuby.framework/Headers/mruby/istruct.h +0 -47
  519. data/assets/xcode/ios/MRuby.framework/Headers/mruby/khash.h +0 -274
  520. data/assets/xcode/ios/MRuby.framework/Headers/mruby/numeric.h +0 -161
  521. data/assets/xcode/ios/MRuby.framework/Headers/mruby/object.h +0 -44
  522. data/assets/xcode/ios/MRuby.framework/Headers/mruby/opcode.h +0 -69
  523. data/assets/xcode/ios/MRuby.framework/Headers/mruby/ops.h +0 -117
  524. data/assets/xcode/ios/MRuby.framework/Headers/mruby/proc.h +0 -131
  525. data/assets/xcode/ios/MRuby.framework/Headers/mruby/range.h +0 -49
  526. data/assets/xcode/ios/MRuby.framework/Headers/mruby/re.h +0 -16
  527. data/assets/xcode/ios/MRuby.framework/Headers/mruby/string.h +0 -444
  528. data/assets/xcode/ios/MRuby.framework/Headers/mruby/throw.h +0 -55
  529. data/assets/xcode/ios/MRuby.framework/Headers/mruby/value.h +0 -309
  530. data/assets/xcode/ios/MRuby.framework/Headers/mruby/variable.h +0 -136
  531. data/assets/xcode/ios/MRuby.framework/Headers/mruby/version.h +0 -110
  532. data/assets/xcode/ios/MRuby.framework/Headers/mruby.h +0 -1282
  533. data/assets/xcode/ios/MRuby.framework/Info.plist +0 -20
  534. data/assets/xcode/ios/MRuby.framework/MRuby +0 -0
  535. data/assets/xcode/ios/MyApp.xcodeproj/project.pbxproj +0 -368
  536. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  537. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  538. data/assets/xcode/ios/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
  539. data/assets/xcode/ios/main.c +0 -1
  540. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
  541. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json +0 -6
  542. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json +0 -17
  543. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
  544. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json +0 -6
  545. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
  546. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json +0 -6
  547. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json +0 -16
  548. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json +0 -6
  549. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json +0 -17
  550. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json +0 -16
  551. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json +0 -6
  552. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json +0 -16
  553. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json +0 -6
  554. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json +0 -32
  555. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json +0 -16
  556. data/assets/xcode/tvos/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json +0 -16
  557. data/assets/xcode/tvos/Assets.xcassets/Contents.json +0 -6
  558. data/assets/xcode/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json +0 -22
  559. data/assets/xcode/tvos/Info.plist +0 -30
  560. data/assets/xcode/tvos/MRuby.framework/Headers/mrbconf.h +0 -143
  561. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/array.h +0 -280
  562. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_nan.h +0 -102
  563. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_no.h +0 -56
  564. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/boxing_word.h +0 -144
  565. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/class.h +0 -97
  566. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/common.h +0 -77
  567. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/compile.h +0 -195
  568. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/data.h +0 -75
  569. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/debug.h +0 -67
  570. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/dump.h +0 -196
  571. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/error.h +0 -75
  572. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/gc.h +0 -91
  573. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/hash.h +0 -215
  574. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/irep.h +0 -75
  575. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/istruct.h +0 -47
  576. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/khash.h +0 -274
  577. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/numeric.h +0 -161
  578. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/object.h +0 -44
  579. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/opcode.h +0 -69
  580. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/ops.h +0 -117
  581. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/proc.h +0 -131
  582. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/range.h +0 -49
  583. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/re.h +0 -16
  584. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/string.h +0 -444
  585. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/throw.h +0 -55
  586. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/value.h +0 -309
  587. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/variable.h +0 -136
  588. data/assets/xcode/tvos/MRuby.framework/Headers/mruby/version.h +0 -110
  589. data/assets/xcode/tvos/MRuby.framework/Headers/mruby.h +0 -1282
  590. data/assets/xcode/tvos/MRuby.framework/Info.plist +0 -20
  591. data/assets/xcode/tvos/MRuby.framework/MRuby +0 -0
  592. data/assets/xcode/tvos/MyApp.xcodeproj/project.pbxproj +0 -350
  593. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  594. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  595. data/assets/xcode/tvos/MyApp.xcodeproj/project.xcworkspace/xcshareddata/MyApp.xcscmblueprint +0 -30
  596. data/assets/xcode/tvos/main.c +0 -1
  597. data/ext/ruby2d/common.c +0 -185
  598. data/ext/ruby2d/controllers.c +0 -110
  599. data/ext/ruby2d/gl.c +0 -398
  600. data/ext/ruby2d/gl2.c +0 -86
  601. data/ext/ruby2d/gl3.c +0 -304
  602. data/ext/ruby2d/gles.c +0 -302
  603. data/ext/ruby2d/input.c +0 -48
  604. data/ext/ruby2d/music.c +0 -127
  605. data/ext/ruby2d/sound.c +0 -132
  606. data/lib/ruby2d/cli/console.rb +0 -65
  607. data/lib/ruby2d/cli/enable_console.rb +0 -7
  608. data/lib/ruby2d/cli/platform.rb +0 -17
  609. data/lib/ruby2d/music.rb +0 -77
  610. data/lib/ruby2d/pixel.rb +0 -18
  611. data/lib/ruby2d/pixmap.rb +0 -39
  612. data/lib/ruby2d/pixmap_atlas.rb +0 -56
  613. data/lib/ruby2d/sound.rb +0 -61
  614. data/lib/ruby2d/texture.rb +0 -38
  615. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2.h +0 -0
  616. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2ext.h +0 -0
  617. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_gl2platform.h +0 -0
  618. /data/assets/{include/SDL2 → platform/include/SDL3}/SDL_opengles2_khrplatform.h +0 -0
  619. /data/assets/{include → platform/include}/mruby/re.h +0 -0
data/ext/ruby2d/window.c CHANGED
@@ -1,385 +1,1099 @@
1
1
  // window.c
2
2
 
3
3
  #include "ruby2d.h"
4
+ #include <math.h> // isinf, for the Float::INFINITY (uncapped) fps_cap path
5
+
6
+
7
+ // =============================================================================
8
+ // Global Variables
9
+ // =============================================================================
10
+
11
+ R2D_DEFINE_DATA_TYPE(R2D_Window);
12
+
13
+ // The single active window instance. Other files access it via R2D_Get*() functions.
14
+ static R2D_Window *r2d_window = NULL;
15
+
16
+ // Ruby VALUE for the Window object — used by the mruby tick trampoline.
17
+ R_VAL ruby2d_window;
18
+
19
+ R2D_Window *R2D_GetWindow(void) { return r2d_window; }
20
+ SDL_Renderer *R2D_GetRenderer(void) { return r2d_window ? r2d_window->sdl_renderer : NULL; }
21
+ float R2D_GetAssetScale(void) { return r2d_window ? r2d_window->asset_scale : 1.0f; }
22
+ float R2D_GetDisplayScale(void) { return r2d_window ? r2d_window->display_scale : 1.0f; }
23
+ void R2D_SetWindow(R2D_Window *w) { r2d_window = w; }
24
+
25
+ // Window-specific ivar IDs. Shared id_width / id_height live in ext.c.
26
+ static R_ID id_title, id_icon, id_ext_window;
27
+ static R_ID id_resizable, id_highdpi, id_pixel_scale;
28
+ static R_ID id_viewport_mode;
29
+ static R_ID id_fps_cap, id_render_mode;
30
+ static R_ID id_background, id_mouse_x, id_mouse_y;
31
+ static R_ID id_frames, id_fps, id_close;
4
32
 
5
33
 
6
34
  /*
7
- * Create a window
35
+ * Initialize
8
36
  */
9
- R2D_Window *R2D_CreateWindow(const char *title, int width, int height,
10
- R2D_Update update, R2D_Render render, int flags) {
37
+ void R2D_Window_Init() {
38
+ id_title = r_id("@title");
39
+ id_icon = r_id("@icon");
40
+ id_ext_window = r_id("@ext_window");
41
+ id_resizable = r_id("@resizable");
42
+ id_highdpi = r_id("@highdpi");
43
+ id_pixel_scale = r_id("@pixel_scale");
44
+ id_viewport_mode = r_id("@viewport_mode");
45
+ id_fps_cap = r_id("@fps_cap");
46
+ id_render_mode = r_id("@render_mode");
47
+ id_background = r_id("@background");
48
+ id_mouse_x = r_id("@mouse_x");
49
+ id_mouse_y = r_id("@mouse_y");
50
+ id_frames = r_id("@frames");
51
+ id_fps = r_id("@fps");
52
+ id_close = r_id("@close");
53
+
54
+ r_define_class_method(ruby2d_ext_module, "window_create", ruby2d_ext_window_create, r_args_variadic);
55
+ r_define_class_method(ruby2d_ext_module, "window_show", ruby2d_ext_window_show, r_args_variadic);
56
+ r_define_class_method(ruby2d_ext_module, "poll_events", ruby2d_ext_window_poll_events, r_args_variadic);
57
+ r_define_class_method(ruby2d_ext_module, "drain_events", ruby2d_ext_window_drain_events, r_args_variadic);
58
+ r_define_class_method(ruby2d_ext_module, "scancode_name", ruby2d_ext_window_scancode_name, r_args_variadic);
59
+ r_define_class_method(ruby2d_ext_module, "begin_frame", ruby2d_ext_window_begin_frame, r_args_variadic);
60
+ r_define_class_method(ruby2d_ext_module, "end_frame", ruby2d_ext_window_end_frame, r_args_variadic);
61
+ r_define_class_method(ruby2d_ext_module, "now", ruby2d_ext_now, r_args_variadic);
62
+ r_define_class_method(ruby2d_ext_module, "window_get_display_dimensions", ruby2d_ext_window_get_display_dimensions, r_args_variadic);
63
+ r_define_class_method(ruby2d_ext_module, "window_set_viewport_mode", ruby2d_ext_window_set_viewport_mode, r_args_variadic);
64
+ r_define_class_method(ruby2d_ext_module, "window_set_icon", ruby2d_ext_window_set_icon, r_args_variadic);
65
+ r_define_class_method(ruby2d_ext_module, "window_set_title", ruby2d_ext_window_set_title, r_args_variadic);
66
+ r_define_class_method(ruby2d_ext_module, "window_set_resizable", ruby2d_ext_window_set_resizable, r_args_variadic);
67
+ r_define_class_method(ruby2d_ext_module, "window_set_size", ruby2d_ext_window_set_size, r_args_variadic);
68
+ r_define_class_method(ruby2d_ext_module, "window_diagnostics", ruby2d_ext_window_diagnostics, r_args_variadic);
69
+ r_define_class_method(ruby2d_ext_module, "window_set_fps_cap", ruby2d_ext_window_set_fps_cap, r_args_variadic);
70
+ r_define_class_method(ruby2d_ext_module, "window_show_fps", ruby2d_ext_window_show_fps, r_args_variadic);
71
+ r_define_class_method(ruby2d_ext_module, "window_load_gamepad_mappings_file", ruby2d_ext_window_load_gamepad_mappings_file, r_args_variadic);
72
+ r_define_class_method(ruby2d_ext_module, "window_add_gamepad_mapping", ruby2d_ext_window_add_gamepad_mapping, r_args_variadic);
73
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_rumble", ruby2d_ext_window_gamepad_rumble, r_args_variadic);
74
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_rumble_triggers", ruby2d_ext_window_gamepad_rumble_triggers, r_args_variadic);
75
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_set_led", ruby2d_ext_window_gamepad_set_led, r_args_variadic);
76
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_type", ruby2d_ext_window_gamepad_type, r_args_variadic);
77
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_battery", ruby2d_ext_window_gamepad_battery, r_args_variadic);
78
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_has_button", ruby2d_ext_window_gamepad_has_button, r_args_variadic);
79
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_has_axis", ruby2d_ext_window_gamepad_has_axis, r_args_variadic);
80
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_caps", ruby2d_ext_window_gamepad_caps, r_args_variadic);
81
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_debug_info", ruby2d_ext_window_gamepad_debug_info, r_args_variadic);
82
+ r_define_class_method(ruby2d_ext_module, "window_gamepad_joystick_state", ruby2d_ext_window_gamepad_joystick_state, r_args_variadic);
83
+ r_define_class_method(ruby2d_ext_module, "window_screenshot", ruby2d_ext_window_screenshot, r_args_variadic);
84
+ r_define_class_method(ruby2d_ext_module, "window_show_cursor", ruby2d_ext_window_show_cursor, r_args_variadic);
85
+ r_define_class_method(ruby2d_ext_module, "window_hide_cursor", ruby2d_ext_window_hide_cursor, r_args_variadic);
86
+ r_define_class_method(ruby2d_ext_module, "window_cursor_visible", ruby2d_ext_window_cursor_visible, r_args_variadic);
87
+ r_define_class_method(ruby2d_ext_module, "window_set_system_cursor", ruby2d_ext_window_set_system_cursor, r_args_variadic);
88
+ r_define_class_method(ruby2d_ext_module, "window_close", ruby2d_ext_window_close, r_args_variadic);
89
+ r_define_class_method(ruby2d_ext_module, "window_set_render_mode", ruby2d_ext_window_set_render_mode, r_args_variadic);
90
+ r_define_class_method(ruby2d_ext_module, "window_request_render", ruby2d_ext_window_request_render, r_args_variadic);
91
+ }
11
92
 
12
- R2D_Init();
13
93
 
14
- SDL_DisplayMode dm;
15
- SDL_GetCurrentDisplayMode(0, &dm);
16
- R2D_Log(R2D_INFO, "Current display mode is %dx%dpx @ %dhz", dm.w, dm.h, dm.refresh_rate);
94
+ // =============================================================================
95
+ // Viewport Mode
96
+ // =============================================================================
17
97
 
18
- width = width == R2D_DISPLAY_WIDTH ? dm.w : width;
19
- height = height == R2D_DISPLAY_HEIGHT ? dm.h : height;
98
+ /*
99
+ * Parse a viewport mode symbol from Ruby into an R2D_VIEWPORT_* constant.
100
+ * Returns R2D_VIEWPORT_LETTERBOX if the value is nil or unrecognized.
101
+ */
102
+ static int R2D_ParseViewportMode(R_VAL vm_val) {
103
+ if (r_test(vm_val)) {
104
+ R_ID vm_id = r_sym_to_id(vm_val);
105
+ if (vm_id == r_id("letterbox")) return R2D_VIEWPORT_LETTERBOX;
106
+ else if (vm_id == r_id("stretch")) return R2D_VIEWPORT_STRETCH;
107
+ else if (vm_id == r_id("integer")) return R2D_VIEWPORT_INTEGER;
108
+ else if (vm_id == r_id("overscan")) return R2D_VIEWPORT_OVERSCAN;
109
+ else if (vm_id == r_id("expand")) return R2D_VIEWPORT_EXPAND;
110
+ else if (vm_id == r_id("fixed")) return R2D_VIEWPORT_FIXED;
111
+ }
112
+ return R2D_VIEWPORT_LETTERBOX;
113
+ }
20
114
 
21
- // Allocate window and set default values
22
- R2D_Window *window = (R2D_Window *) malloc(sizeof(R2D_Window));
23
- window->sdl = NULL;
24
- window->glcontext = NULL;
25
- window->title = title;
26
- window->width = width;
27
- window->height = height;
28
- window->orig_width = width;
29
- window->orig_height = height;
30
- window->viewport.width = width;
31
- window->viewport.height = height;
32
- window->viewport.mode = R2D_SCALE;
33
- window->update = update;
34
- window->render = render;
35
- window->flags = flags;
36
- window->on_key = NULL;
37
- window->on_mouse = NULL;
38
- window->on_controller = NULL;
39
- window->vsync = true;
40
- window->fps_cap = 60;
41
- window->background.r = 0.0;
42
- window->background.g = 0.0;
43
- window->background.b = 0.0;
44
- window->background.a = 1.0;
45
- window->icon = NULL;
46
- window->close = true;
47
115
 
48
- // Return the window structure
49
- return window;
50
- }
116
+ /*
117
+ * Compute and apply the fixed-mode viewport rect. Centers the viewport at 1:1
118
+ * scale within the window, with no content scaling. The rect is in logical
119
+ * (point) coordinates because SDL_SetRenderViewport interprets its rect in the
120
+ * current scaled coordinate space.
121
+ */
122
+ static void R2D_ApplyFixedViewport(R2D_Window *window) {
123
+ float ds = (window->pixel_scale && (window->flags & R2D_HIGHDPI))
124
+ ? 1.0f : window->display_scale;
125
+ SDL_SetRenderScale(window->sdl_renderer, ds, ds);
51
126
 
127
+ int vp_w = window->viewport.width;
128
+ int vp_h = window->viewport.height;
52
129
 
53
- const Uint8 *key_state;
130
+ window->viewport.fixed_rect.x = (window->width - vp_w) / 2;
131
+ window->viewport.fixed_rect.y = (window->height - vp_h) / 2;
132
+ window->viewport.fixed_rect.w = vp_w;
133
+ window->viewport.fixed_rect.h = vp_h;
54
134
 
55
- Uint32 frames;
56
- Uint32 frames_last_sec;
57
- Uint32 start_ms;
58
- Uint32 next_second_ms;
59
- Uint32 begin_ms;
60
- Uint32 end_ms; // Time at end of loop
61
- Uint32 elapsed_ms; // Total elapsed time
62
- Uint32 loop_ms; // Elapsed time of current loop
63
- int delay_ms; // Amount of delay to achieve desired frame rate
64
- double decay_rate;
65
- double fps;
135
+ SDL_SetRenderViewport(window->sdl_renderer, &window->viewport.fixed_rect);
136
+ }
66
137
 
67
- R2D_Window *window;
68
138
 
139
+ /*
140
+ * Apply viewport scaling mode via SDL3 logical presentation.
141
+ * Maps the R2D_VIEWPORT_* constants to SDL_RendererLogicalPresentation values
142
+ * and configures the renderer accordingly. Fixed mode bypasses logical
143
+ * presentation entirely and uses a manually centered viewport at 1:1 scale.
144
+ */
145
+ void R2D_ApplyViewportMode(R2D_Window *window) {
69
146
 
147
+ // Fixed mode: disable logical presentation, manage viewport manually
148
+ if (window->viewport.mode == R2D_VIEWPORT_FIXED) {
149
+ SDL_SetRenderLogicalPresentation(
150
+ window->sdl_renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED
151
+ );
152
+ R2D_ApplyFixedViewport(window);
153
+ return;
154
+ }
70
155
 
156
+ // All other modes use SDL logical presentation — reset any manual viewport
157
+ // and scale that fixed mode may have set.
158
+ SDL_SetRenderViewport(window->sdl_renderer, NULL);
159
+ SDL_SetRenderScale(window->sdl_renderer, 1.0f, 1.0f);
160
+
161
+ SDL_RendererLogicalPresentation sdl_mode;
162
+
163
+ switch (window->viewport.mode) {
164
+ case R2D_VIEWPORT_STRETCH:
165
+ sdl_mode = SDL_LOGICAL_PRESENTATION_STRETCH;
166
+ break;
167
+ case R2D_VIEWPORT_INTEGER:
168
+ sdl_mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
169
+ break;
170
+ case R2D_VIEWPORT_OVERSCAN:
171
+ sdl_mode = SDL_LOGICAL_PRESENTATION_OVERSCAN;
172
+ break;
173
+ case R2D_VIEWPORT_EXPAND:
174
+ case R2D_VIEWPORT_LETTERBOX:
175
+ default:
176
+ sdl_mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
177
+ break;
178
+ }
71
179
 
180
+ if (!SDL_SetRenderLogicalPresentation(
181
+ window->sdl_renderer,
182
+ window->viewport.width,
183
+ window->viewport.height,
184
+ sdl_mode
185
+ )) {
186
+ R2D_Error("R2D_ApplyViewportMode", "SDL_SetRenderLogicalPresentation failed: %s", SDL_GetError());
187
+ }
188
+ }
72
189
 
73
- void main_loop() {
74
190
 
75
- // Clear Frame /////////////////////////////////////////////////////////////
191
+ /*
192
+ * Derive the render (physical) dimensions from the logical, user-intended
193
+ * sizes: window `orig_width`/`orig_height` and `viewport.base_width`/`base_height`.
194
+ * When pixel_scale is active on a HiDPI window, scale by display_scale once so
195
+ * coordinates map 1:1 with physical pixels (and asset_scale drops to 1.0, since
196
+ * assets are already authored in physical pixels); otherwise assets carry the
197
+ * display scale. Idempotent by construction — it always recomputes from the
198
+ * logical sizes, never from the current (possibly already-scaled) values — so it
199
+ * can be re-applied on every set()/resize without compounding. Expects
200
+ * `viewport.mode` and `pixel_scale` to be current.
201
+ */
202
+ static void R2D_ApplyPixelScale(R2D_Window *window) {
203
+ bool ps_active = window->pixel_scale && (window->flags & R2D_HIGHDPI);
204
+ float ds = ps_active ? window->display_scale : 1.0f;
205
+
206
+ window->width = (int)(window->orig_width * ds);
207
+ window->height = (int)(window->orig_height * ds);
208
+
209
+ // In expand mode the viewport always matches the window; other modes keep a
210
+ // fixed logical canvas (base_*) that scales with pixel_scale.
211
+ if (window->viewport.mode == R2D_VIEWPORT_EXPAND) {
212
+ window->viewport.width = window->width;
213
+ window->viewport.height = window->height;
214
+ } else {
215
+ window->viewport.width = (int)(window->viewport.base_width * ds);
216
+ window->viewport.height = (int)(window->viewport.base_height * ds);
217
+ }
76
218
 
77
- R2D_GL_Clear(window->background);
219
+ window->asset_scale = ps_active ? 1.0f : window->display_scale;
220
+ }
78
221
 
79
- // Set FPS /////////////////////////////////////////////////////////////////
80
222
 
81
- frames++;
82
- frames_last_sec++;
83
- end_ms = SDL_GetTicks();
84
- elapsed_ms = end_ms - start_ms;
223
+ // =============================================================================
224
+ // Render Mode
225
+ // =============================================================================
85
226
 
86
- // Calculate the frame rate using an exponential moving average
87
- if (next_second_ms < end_ms) {
88
- fps = decay_rate * fps + (1.0 - decay_rate) * frames_last_sec;
89
- frames_last_sec = 0;
90
- next_second_ms = SDL_GetTicks() + 1000;
227
+ /*
228
+ * Parse a render mode symbol from Ruby into an R2D_RENDER_* constant.
229
+ * Returns R2D_RENDER_CONTINUOUS if the value is nil or unrecognized.
230
+ */
231
+ static int R2D_ParseRenderMode(R_VAL rm_val) {
232
+ if (r_test(rm_val)) {
233
+ R_ID rm_id = r_sym_to_id(rm_val);
234
+ if (rm_id == r_id("on_demand")) return R2D_RENDER_ON_DEMAND;
91
235
  }
236
+ return R2D_RENDER_CONTINUOUS;
237
+ }
92
238
 
93
- loop_ms = end_ms - begin_ms;
94
- delay_ms = (1000 / window->fps_cap) - loop_ms;
95
239
 
96
- if (delay_ms < 0) delay_ms = 0;
240
+ // =============================================================================
241
+ // Window Creation and Setup
242
+ // =============================================================================
97
243
 
98
- // Note: `loop_ms + delay_ms` should equal `1000 / fps_cap`
244
+ /*
245
+ * Ruby2D::Window#ext_create
246
+ */
247
+ R_VAL ruby2d_ext_window_create(RUBY2D_METHOD_ARGS_VARIADIC) {
248
+ RUBY2D_EXTRACT_VARIADIC;
249
+ if (argc != 1) r_raise("Ruby2D::Ext.window_create expects 1 arg (window), got %d", (int)argc);
250
+ R_VAL obj = argv[0];
251
+
252
+ if (!R2D_Init()) r_raise("Ruby2D: failed to initialize: %s", SDL_GetError());
253
+
254
+ int width = obj_int(obj, id_width);
255
+ int height = obj_int(obj, id_height);
256
+
257
+ // Use display dimensions from init-time query if requested
258
+ if (width == R2D_DISPLAY_WIDTH) width = R2D_GetInitDisplayWidth();
259
+ if (height == R2D_DISPLAY_HEIGHT) height = R2D_GetInitDisplayHeight();
260
+
261
+ // Allocate window and set default values. Use calloc so every field starts
262
+ // zeroed — defends against reads of any field not explicitly set below (e.g.
263
+ // viewport.fixed_rect) and against future field additions.
264
+ R2D_Window *window = (R2D_Window *) calloc(1, sizeof(R2D_Window));
265
+ if (!window) r_raise("Ruby2D: failed to allocate window");
266
+ // Set the title from the Ruby instance variable so the native window
267
+ // gets the requested title.
268
+ window->title = obj_str(obj, id_title);
269
+ // Determine initial window flags from Ruby instance variables (resizable,
270
+ // highdpi) and store them on the native window.
271
+ int init_flags = 0;
272
+ if (r_test(r_ivar_get(obj, id_resizable))) init_flags |= R2D_RESIZABLE;
273
+ if (r_test(r_ivar_get(obj, id_highdpi))) init_flags |= R2D_HIGHDPI;
274
+ window->flags = init_flags;
99
275
 
100
- #ifndef __EMSCRIPTEN__
101
- SDL_Delay(delay_ms);
102
- #endif
276
+ window->width = width;
277
+ window->height = height;
278
+ window->orig_width = width;
279
+ window->orig_height = height;
280
+ // Read viewport dimensions from Ruby (fall back to width/height)
281
+ R_VAL vw_val = r_ivar_get(obj, id_viewport_width);
282
+ R_VAL vh_val = r_ivar_get(obj, id_viewport_height);
283
+ window->viewport.width = r_test(vw_val) ? NUM2INT(vw_val) : width;
284
+ window->viewport.height = r_test(vh_val) ? NUM2INT(vh_val) : height;
285
+ window->viewport.base_width = window->viewport.width;
286
+ window->viewport.base_height = window->viewport.height;
287
+
288
+ // Parse viewport mode symbol from Ruby
289
+ window->viewport.mode = R2D_ParseViewportMode(r_ivar_get(obj, id_viewport_mode));
290
+
291
+ window->fps_cap = (int)R2D_GetInitRefreshRate();
292
+ window->background.r = 0.0;
293
+ window->background.g = 0.0;
294
+ window->background.b = 0.0;
295
+ window->background.a = 1.0;
296
+ window->icon = NULL;
297
+ window->close = true;
298
+ window->pixel_scale = obj_bool(obj, id_pixel_scale);
299
+ window->render_mode = R2D_ParseRenderMode(r_ivar_get(obj, id_render_mode));
300
+
301
+ // Start with render_pending set so the first tick always presents a frame,
302
+ // even in on-demand mode before the app calls request_render.
303
+ SDL_SetAtomicInt(&window->render_pending, 1);
304
+
305
+ // Copy display info from pre-window statics (populated by R2D_Init)
306
+ int dw = R2D_GetInitDisplayWidth();
307
+ int dh = R2D_GetInitDisplayHeight();
308
+ window->display_width = dw ? dw : width;
309
+ window->display_height = dh ? dh : height;
310
+ window->display_content_scale = R2D_GetInitContentScale();
311
+ window->display_scale = R2D_GetInitDisplayScale();
312
+ window->display_refresh_rate = R2D_GetInitRefreshRate();
313
+ window->asset_scale = R2D_GetInitDisplayScale();
314
+ window->display_pixel_width = R2D_GetInitDisplayPixelWidth();
315
+ window->display_pixel_height = R2D_GetInitDisplayPixelHeight();
316
+ window->diagnostics = false;
317
+ window->show_fps = false;
318
+ window->screenshot_path = NULL;
319
+
320
+ // SDL window and renderer are created later, in R2D_Show.
321
+ r2d_window = window;
322
+
323
+ obj_set_struct(obj, id_ext_window, R2D_Window, window);
324
+
325
+ return R_TRUE;
326
+ }
327
+
328
+
329
+ // =============================================================================
330
+ // Per-Frame Ext Bindings
331
+ // =============================================================================
332
+ //
333
+ // Ruby owns frame sequencing via its `tick` method, which calls these Ext
334
+ // primitives each frame: poll_events → drain_events → begin_frame → end_frame.
335
+ // Events are queued into a C-side buffer and returned to Ruby as a flat array.
336
+
337
+ // Frame timing statics
338
+ Uint64 frames;
339
+ Uint64 perf_freq;
340
+ double target_frame;
341
+ static Uint64 frame_start;
342
+ static bool current_frame_rendered;
343
+
344
+ // Event buffer — grows as needed, reset each frame, never freed.
345
+ static R2D_QueuedEvent *event_buf = NULL;
346
+ static int event_count = 0;
347
+ static int event_capacity = 0;
348
+
349
+ static void event_buf_push(R2D_QueuedEvent ev) {
350
+ if (event_count == event_capacity) {
351
+ int new_capacity = event_capacity ? event_capacity * 2 : 64;
352
+ R2D_QueuedEvent *grown = (R2D_QueuedEvent *)realloc(event_buf,
353
+ new_capacity * sizeof(R2D_QueuedEvent));
354
+ // On failure realloc leaves the old buffer intact; drop this event rather
355
+ // than overwrite event_buf with NULL and crash on the next write.
356
+ if (!grown) return;
357
+ event_buf = grown;
358
+ event_capacity = new_capacity;
359
+ }
360
+ event_buf[event_count++] = ev;
361
+ }
103
362
 
104
- begin_ms = SDL_GetTicks();
363
+ // Initial gamepad scan — runs once on the first poll_events call so that
364
+ // connect events for already-plugged-in pads appear in the first drain.
365
+ static bool first_poll = true;
105
366
 
106
- // Handle Input and Window Events //////////////////////////////////////////
107
367
 
108
- int mx, my; // mouse x, y coordinates
368
+ /*
369
+ * Ext.poll_events(window)
370
+ *
371
+ * Poll SDL events and held-input state, queue into the event buffer,
372
+ * update timing, and sync C state to Ruby ivars.
373
+ */
374
+ R_VAL ruby2d_ext_window_poll_events(RUBY2D_METHOD_ARGS_VARIADIC) {
375
+ RUBY2D_EXTRACT_VARIADIC;
376
+ if (argc != 1) r_raise("Ruby2D::Ext.poll_events expects 1 arg (window), got %d", (int)argc);
377
+ R_VAL obj = argv[0];
378
+
379
+ frame_start = SDL_GetPerformanceCounter();
380
+ event_count = 0;
381
+
382
+ // On the first call, push connect events for already-plugged-in gamepads
383
+ if (first_poll) {
384
+ first_poll = false;
385
+ int num_pads = 0;
386
+ SDL_JoystickID *pads = SDL_GetGamepads(&num_pads);
387
+ if (pads) {
388
+ for (int i = 0; i < num_pads; i++) {
389
+ SDL_JoystickID gid = pads[i];
390
+ SDL_Gamepad *pad = SDL_GetGamepadFromID(gid);
391
+ if (!pad) pad = SDL_OpenGamepad(gid);
392
+ if (!pad) continue;
393
+ // Own the name: drain frees it. SDL_GetGamepadName returns a borrowed
394
+ // pointer that SDL_CloseGamepad frees, so copy it now (see drain).
395
+ const char *name = SDL_GetGamepadName(pad);
396
+ event_buf_push((R2D_QueuedEvent){
397
+ .category = R2D_EVT_GAMEPAD, .type = R2D_GAMEPAD_CONNECT,
398
+ .id = gid, .str = name ? SDL_strdup(name) : NULL
399
+ });
400
+ }
401
+ SDL_free(pads);
402
+ }
403
+ }
404
+
405
+ // Poll SDL events ///////////////////////////////////////////////////////////
109
406
 
110
407
  SDL_Event e;
111
408
  while (SDL_PollEvent(&e)) {
112
- switch (e.type) {
409
+ SDL_ConvertEventToRenderCoordinates(r2d_window->sdl_renderer, &e);
113
410
 
114
- case SDL_KEYDOWN:
115
- if (window->on_key && e.key.repeat == 0) {
116
- R2D_Event event = {
117
- .type = R2D_KEY_DOWN, .key = SDL_GetScancodeName(e.key.keysym.scancode)
118
- };
119
- window->on_key(event);
411
+ switch (e.type) {
412
+ case SDL_EVENT_KEY_DOWN:
413
+ if (e.key.repeat == 0) {
414
+ event_buf_push((R2D_QueuedEvent){
415
+ .category = R2D_EVT_KEY, .type = R2D_KEY_DOWN,
416
+ .id = e.key.scancode
417
+ });
120
418
  }
121
419
  break;
122
420
 
123
- case SDL_KEYUP:
124
- if (window->on_key) {
125
- R2D_Event event = {
126
- .type = R2D_KEY_UP, .key = SDL_GetScancodeName(e.key.keysym.scancode)
127
- };
128
- window->on_key(event);
129
- }
421
+ case SDL_EVENT_KEY_UP:
422
+ event_buf_push((R2D_QueuedEvent){
423
+ .category = R2D_EVT_KEY, .type = R2D_KEY_UP,
424
+ .id = e.key.scancode
425
+ });
130
426
  break;
131
427
 
132
- case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP:
133
- if (window->on_mouse) {
134
- R2D_GetMouseOnViewport(window, e.button.x, e.button.y, &mx, &my);
135
- R2D_Event event = {
136
- .button = e.button.button, .x = mx, .y = my
137
- };
138
- event.type = e.type == SDL_MOUSEBUTTONDOWN ? R2D_MOUSE_DOWN : R2D_MOUSE_UP;
139
- event.dblclick = e.button.clicks == 2 ? true : false;
140
- window->on_mouse(event);
141
- }
428
+ case SDL_EVENT_MOUSE_BUTTON_DOWN:
429
+ case SDL_EVENT_MOUSE_BUTTON_UP:
430
+ event_buf_push((R2D_QueuedEvent){
431
+ .category = R2D_EVT_MOUSE,
432
+ .type = e.type == SDL_EVENT_MOUSE_BUTTON_DOWN ? R2D_MOUSE_DOWN : R2D_MOUSE_UP,
433
+ .button = e.button.button,
434
+ .x = (int)e.button.x, .y = (int)e.button.y
435
+ });
142
436
  break;
143
437
 
144
- case SDL_MOUSEWHEEL:
145
- if (window->on_mouse) {
146
- R2D_Event event = {
147
- .type = R2D_MOUSE_SCROLL, .direction = e.wheel.direction,
148
- .delta_x = e.wheel.x, .delta_y = -e.wheel.y
149
- };
150
- window->on_mouse(event);
151
- }
438
+ case SDL_EVENT_MOUSE_WHEEL:
439
+ event_buf_push((R2D_QueuedEvent){
440
+ .category = R2D_EVT_MOUSE, .type = R2D_MOUSE_SCROLL,
441
+ .direction = e.wheel.direction,
442
+ .delta_x = e.wheel.x, .delta_y = -e.wheel.y
443
+ });
152
444
  break;
153
445
 
154
- case SDL_MOUSEMOTION:
155
- if (window->on_mouse) {
156
- R2D_GetMouseOnViewport(window, e.motion.x, e.motion.y, &mx, &my);
157
- R2D_Event event = {
158
- .type = R2D_MOUSE_MOVE,
159
- .x = mx, .y = my, .delta_x = e.motion.xrel, .delta_y = e.motion.yrel
160
- };
161
- window->on_mouse(event);
162
- }
446
+ case SDL_EVENT_MOUSE_MOTION:
447
+ event_buf_push((R2D_QueuedEvent){
448
+ .category = R2D_EVT_MOUSE, .type = R2D_MOUSE_MOVE,
449
+ .x = (int)e.motion.x, .y = (int)e.motion.y,
450
+ .delta_x = e.motion.xrel, .delta_y = e.motion.yrel
451
+ });
163
452
  break;
164
453
 
165
- case SDL_CONTROLLERAXISMOTION:
166
- if (window->on_controller) {
167
- R2D_Event event = {
168
- .which = e.caxis.which, .type = R2D_AXIS,
169
- .axis = e.caxis.axis, .value = e.caxis.value
170
- };
171
- window->on_controller(event);
172
- }
454
+ case SDL_EVENT_GAMEPAD_AXIS_MOTION:
455
+ event_buf_push((R2D_QueuedEvent){
456
+ .category = R2D_EVT_GAMEPAD, .type = R2D_GAMEPAD_AXIS,
457
+ .id = e.gaxis.which, .axis = e.gaxis.axis, .value = e.gaxis.value
458
+ });
173
459
  break;
174
460
 
175
- case SDL_JOYAXISMOTION:
176
- if (window->on_controller && !R2D_IsController(e.jbutton.which)) {
177
- R2D_Event event = {
178
- .which = e.jaxis.which, .type = R2D_AXIS,
179
- .axis = e.jaxis.axis, .value = e.jaxis.value
180
- };
181
- window->on_controller(event);
182
- }
461
+ case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
462
+ case SDL_EVENT_GAMEPAD_BUTTON_UP:
463
+ event_buf_push((R2D_QueuedEvent){
464
+ .category = R2D_EVT_GAMEPAD,
465
+ .type = e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN
466
+ ? R2D_GAMEPAD_BUTTON_DOWN : R2D_GAMEPAD_BUTTON_UP,
467
+ .id = e.gbutton.which, .button = e.gbutton.button
468
+ });
183
469
  break;
184
470
 
185
- case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP:
186
- if (window->on_controller) {
187
- R2D_Event event = {
188
- .which = e.cbutton.which, .button = e.cbutton.button
189
- };
190
- event.type = e.type == SDL_CONTROLLERBUTTONDOWN ? R2D_BUTTON_DOWN : R2D_BUTTON_UP;
191
- window->on_controller(event);
471
+ case SDL_EVENT_GAMEPAD_ADDED: {
472
+ SDL_JoystickID gid = e.gdevice.which;
473
+ SDL_Gamepad *pad = SDL_OpenGamepad(gid);
474
+ if (!pad) {
475
+ R2D_Error("SDL_OpenGamepad", "SDL_OpenGamepad(%u) failed: %s", (unsigned)gid, SDL_GetError());
476
+ } else {
477
+ // Own the name: drain frees it. SDL_GetGamepadName returns a borrowed
478
+ // pointer that SDL_CloseGamepad frees, so copy it now (see drain).
479
+ const char *name = SDL_GetGamepadName(pad);
480
+ R2D_Log(R2D_INFO, "Gamepad added: id=%u, name=%s", (unsigned)gid, name);
481
+ event_buf_push((R2D_QueuedEvent){
482
+ .category = R2D_EVT_GAMEPAD, .type = R2D_GAMEPAD_CONNECT,
483
+ .id = gid, .str = name ? SDL_strdup(name) : NULL
484
+ });
192
485
  }
193
486
  break;
487
+ }
194
488
 
195
- case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP:
196
- if (window->on_controller && !R2D_IsController(e.jbutton.which)) {
197
- R2D_Event event = {
198
- .which = e.jbutton.which, .button = e.jbutton.button
199
- };
200
- event.type = e.type == SDL_JOYBUTTONDOWN ? R2D_BUTTON_DOWN : R2D_BUTTON_UP;
201
- window->on_controller(event);
202
- }
489
+ case SDL_EVENT_GAMEPAD_REMOVED: {
490
+ SDL_JoystickID gid = e.gdevice.which;
491
+ SDL_Gamepad *pad = SDL_GetGamepadFromID(gid);
492
+ R2D_Log(R2D_INFO, "Gamepad removed: id=%u", (unsigned)gid);
493
+ event_buf_push((R2D_QueuedEvent){
494
+ .category = R2D_EVT_GAMEPAD, .type = R2D_GAMEPAD_DISCONNECT,
495
+ .id = gid
496
+ });
497
+ if (pad) SDL_CloseGamepad(pad);
203
498
  break;
499
+ }
204
500
 
205
- case SDL_JOYDEVICEADDED:
206
- R2D_Log(R2D_INFO, "Controller connected (%i total)", SDL_NumJoysticks());
207
- R2D_OpenControllers();
501
+ case SDL_EVENT_WINDOW_RESIZED:
502
+ // New logical (user-intended) window size from the OS event, before any
503
+ // pixel_scale multiplier. R2D_ApplyPixelScale derives the render size.
504
+ r2d_window->orig_width = (int)(e.window.data1 / r2d_window->display_content_scale);
505
+ r2d_window->orig_height = (int)(e.window.data2 / r2d_window->display_content_scale);
506
+
507
+ // In expand mode the viewport tracks the window, so its logical base
508
+ // follows the new size; fixed/scaled modes keep their base canvas.
509
+ if (r2d_window->viewport.mode == R2D_VIEWPORT_EXPAND) {
510
+ r2d_window->viewport.base_width = r2d_window->orig_width;
511
+ r2d_window->viewport.base_height = r2d_window->orig_height;
512
+ }
513
+
514
+ R2D_ApplyPixelScale(r2d_window);
515
+ R2D_ApplyViewportMode(r2d_window);
516
+
517
+ SDL_SetAtomicInt(&r2d_window->render_pending, 1);
208
518
  break;
209
519
 
210
- case SDL_JOYDEVICEREMOVED:
211
- if (R2D_IsController(e.jdevice.which)) {
212
- R2D_Log(R2D_INFO, "Controller #%i: %s removed (%i remaining)", e.jdevice.which, SDL_GameControllerName(SDL_GameControllerFromInstanceID(e.jdevice.which)), SDL_NumJoysticks());
213
- SDL_GameControllerClose(SDL_GameControllerFromInstanceID(e.jdevice.which));
214
- } else {
215
- R2D_Log(R2D_INFO, "Controller #%i: %s removed (%i remaining)", e.jdevice.which, SDL_JoystickName(SDL_JoystickFromInstanceID(e.jdevice.which)), SDL_NumJoysticks());
216
- SDL_JoystickClose(SDL_JoystickFromInstanceID(e.jdevice.which));
217
- }
520
+ case SDL_EVENT_WINDOW_EXPOSED:
521
+ case SDL_EVENT_WINDOW_SHOWN:
522
+ case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
523
+ SDL_SetAtomicInt(&r2d_window->render_pending, 1);
218
524
  break;
219
525
 
220
- case SDL_WINDOWEVENT:
221
- switch (e.window.event) {
222
- case SDL_WINDOWEVENT_RESIZED:
223
- // Store new window size, set viewport
224
- window->width = e.window.data1;
225
- window->height = e.window.data2;
226
- R2D_GL_SetViewport(window);
227
- break;
228
- }
526
+ case SDL_EVENT_WINDOW_MOUSE_ENTER:
527
+ case SDL_EVENT_WINDOW_MOUSE_LEAVE:
528
+ event_buf_push((R2D_QueuedEvent){
529
+ .category = R2D_EVT_MOUSE,
530
+ .type = e.type == SDL_EVENT_WINDOW_MOUSE_ENTER
531
+ ? R2D_MOUSE_ENTER : R2D_MOUSE_LEAVE
532
+ });
229
533
  break;
230
534
 
231
- case SDL_QUIT:
232
- R2D_Close(window);
535
+ case SDL_EVENT_QUIT:
536
+ event_buf_push((R2D_QueuedEvent){ .category = R2D_EVT_CLOSE });
233
537
  break;
234
538
  }
235
539
  }
236
540
 
237
- // Detect keys held down
541
+ // Detect keys held down. Each held key carries only its integer scancode —
542
+ // Ruby resolves and caches the name once per distinct key, so the per-frame
543
+ // path allocates no name string.
238
544
  int num_keys;
239
- key_state = SDL_GetKeyboardState(&num_keys);
240
-
545
+ const bool *key_state = SDL_GetKeyboardState(&num_keys);
241
546
  for (int i = 0; i < num_keys; i++) {
242
- if (window->on_key) {
243
- if (key_state[i] == 1) {
244
- R2D_Event event = {
245
- .type = R2D_KEY_HELD, .key = SDL_GetScancodeName(i)
246
- };
247
- window->on_key(event);
547
+ if (key_state[i]) {
548
+ event_buf_push((R2D_QueuedEvent){
549
+ .category = R2D_EVT_KEY, .type = R2D_KEY_HELD,
550
+ .id = i
551
+ });
552
+ }
553
+ }
554
+
555
+ // Get and store mouse position in logical renderer coordinates
556
+ float ms_x, ms_y;
557
+ Uint32 mouse_buttons = SDL_GetMouseState(&ms_x, &ms_y);
558
+ float logical_x, logical_y;
559
+ SDL_RenderCoordinatesFromWindow(r2d_window->sdl_renderer, ms_x, ms_y, &logical_x, &logical_y);
560
+ r2d_window->mouse.x = (int)logical_x;
561
+ r2d_window->mouse.y = (int)logical_y;
562
+
563
+ // Detect mouse buttons held down
564
+ static const int mouse_held_buttons[] = {
565
+ R2D_MOUSE_LEFT, R2D_MOUSE_MIDDLE, R2D_MOUSE_RIGHT, R2D_MOUSE_X1, R2D_MOUSE_X2
566
+ };
567
+ for (int i = 0; i < 5; i++) {
568
+ int btn = mouse_held_buttons[i];
569
+ if (mouse_buttons & SDL_BUTTON_MASK(btn)) {
570
+ event_buf_push((R2D_QueuedEvent){
571
+ .category = R2D_EVT_MOUSE, .type = R2D_MOUSE_HELD,
572
+ .button = btn, .x = (int)logical_x, .y = (int)logical_y
573
+ });
574
+ }
575
+ }
576
+
577
+ // Detect gamepad buttons held down. Gated on SDL_HasGamepad(): with no pad
578
+ // connected — the common case — this skips SDL_GetGamepads, whose returned
579
+ // ID array is a per-frame malloc/free that an empty window shouldn't pay.
580
+ if (SDL_HasGamepad()) {
581
+ int num_pads = 0;
582
+ SDL_JoystickID *pads = SDL_GetGamepads(&num_pads);
583
+ if (pads) {
584
+ for (int i = 0; i < num_pads; i++) {
585
+ SDL_Gamepad *pad = SDL_GetGamepadFromID(pads[i]);
586
+ if (!pad) continue;
587
+ for (int b = 0; b < R2D_BUTTON_COUNT; b++) {
588
+ if (SDL_GetGamepadButton(pad, b)) {
589
+ event_buf_push((R2D_QueuedEvent){
590
+ .category = R2D_EVT_GAMEPAD, .type = R2D_GAMEPAD_BUTTON_HELD,
591
+ .id = pads[i], .button = b
592
+ });
593
+ }
594
+ }
248
595
  }
596
+ SDL_free(pads);
249
597
  }
250
598
  }
251
599
 
252
- // Get and store mouse position relative to the viewport
253
- int wx, wy; // mouse x, y coordinates relative to the window
254
- SDL_GetMouseState(&wx, &wy);
255
- R2D_GetMouseOnViewport(window, wx, wy, &window->mouse.x, &window->mouse.y);
600
+ // Sync C state to Ruby ivars. Frame count and fps are deferred to begin_frame
601
+ // so they track frames actually presented, not ticks in :on_demand mode most
602
+ // ticks present nothing.
603
+ r_ivar_set(obj, id_mouse_x, INT2NUM(r2d_window->mouse.x));
604
+ r_ivar_set(obj, id_mouse_y, INT2NUM(r2d_window->mouse.y));
605
+ // width/height are the logical (user-intended) size per USAGE.md — sync
606
+ // orig_width/orig_height, not window->width/height, which carry the physical
607
+ // render size under pixel_scale. viewport_width/height expose the physical size.
608
+ r_ivar_set(obj, id_width, INT2NUM(r2d_window->orig_width));
609
+ r_ivar_set(obj, id_height, INT2NUM(r2d_window->orig_height));
610
+ r_ivar_set(obj, id_viewport_width, INT2NUM(r2d_window->viewport.width));
611
+ r_ivar_set(obj, id_viewport_height, INT2NUM(r2d_window->viewport.height));
612
+
613
+ if (r2d_window->close) r_ivar_set(obj, id_close, R_TRUE);
614
+
615
+ return R_NIL;
616
+ }
256
617
 
257
- // Update Window State /////////////////////////////////////////////////////
258
618
 
259
- // Store new values in the window
260
- window->frames = frames;
261
- window->elapsed_ms = elapsed_ms;
262
- window->loop_ms = loop_ms;
263
- window->delay_ms = delay_ms;
264
- window->fps = fps;
619
+ /*
620
+ * Ext.drain_events(window) → flat Array or nil
621
+ *
622
+ * Return queued events as a flat array with R2D_EVT_STRIDE elements per event:
623
+ * [category, type, id, button, direction, axis, x, y, delta_x, delta_y, value, str]
624
+ * Returns nil when no events are queued (the common case, every frame).
625
+ */
626
+ R_VAL ruby2d_ext_window_drain_events(RUBY2D_METHOD_ARGS_VARIADIC) {
627
+ RUBY2D_EXTRACT_VARIADIC;
628
+ (void)argc; (void)argv;
629
+
630
+ // Most frames have no events — return nil instead of allocating an empty
631
+ // array 60–120 times a second. The Ruby side skips dispatch on nil.
632
+ if (event_count == 0) return R_NIL;
633
+
634
+ R_VAL ary = r_ary_new();
635
+ for (int i = 0; i < event_count; i++) {
636
+ R2D_QueuedEvent *ev = &event_buf[i];
637
+ r_ary_push(ary, INT2NUM(ev->category));
638
+ r_ary_push(ary, INT2NUM(ev->type));
639
+ r_ary_push(ary, INT2NUM(ev->id));
640
+ r_ary_push(ary, INT2NUM(ev->button));
641
+ r_ary_push(ary, INT2NUM(ev->direction));
642
+ r_ary_push(ary, INT2NUM(ev->axis));
643
+ r_ary_push(ary, INT2NUM(ev->x));
644
+ r_ary_push(ary, INT2NUM(ev->y));
645
+ r_ary_push(ary, DBL2NUM(ev->delta_x));
646
+ r_ary_push(ary, DBL2NUM(ev->delta_y));
647
+ r_ary_push(ary, INT2NUM(ev->value));
648
+ r_ary_push(ary, ev->str ? r_str_new(ev->str) : R_NIL);
649
+ // Ownership contract: poll strdups gamepad-connect names into ev->str, drain
650
+ // frees them here. All other events leave ev->str NULL via designated init,
651
+ // so freeing every non-NULL ev->str is correct.
652
+ if (ev->str) {
653
+ SDL_free((void *)ev->str);
654
+ ev->str = NULL;
655
+ }
656
+ }
657
+ event_count = 0;
658
+ return ary;
659
+ }
265
660
 
266
- // Call update and render callbacks
267
- if (window->update) window->update();
268
- if (window->render) window->render();
269
661
 
270
- // Draw Frame //////////////////////////////////////////////////////////////
662
+ /*
663
+ * Ext.scancode_name(code) → String
664
+ *
665
+ * Human-readable name for an SDL scancode (e.g. "Space", "Left Shift"), or ""
666
+ * if the scancode has no name. The per-frame held-key path carries only the
667
+ * integer scancode; Ruby calls this at most once per distinct key and caches
668
+ * the result, so no key-name string is allocated each frame.
669
+ */
670
+ R_VAL ruby2d_ext_window_scancode_name(RUBY2D_METHOD_ARGS_VARIADIC) {
671
+ RUBY2D_EXTRACT_VARIADIC;
672
+ if (argc != 1) r_raise("Ruby2D::Ext.scancode_name expects 1 arg (scancode), got %d", (int)argc);
673
+ const char *name = SDL_GetScancodeName((SDL_Scancode)NUM2INT(argv[0]));
674
+ return r_str_new(name ? name : "");
675
+ }
676
+
271
677
 
272
- // Render and flush all OpenGL buffers
273
- R2D_GL_FlushBuffers();
678
+ /*
679
+ * Ext.begin_frame(window) → true/false
680
+ *
681
+ * Sync background color from Ruby, decide whether to render, and clear.
682
+ * Returns true if Ruby should call render_objects + render_callback.
683
+ */
684
+ R_VAL ruby2d_ext_window_begin_frame(RUBY2D_METHOD_ARGS_VARIADIC) {
685
+ RUBY2D_EXTRACT_VARIADIC;
686
+ if (argc != 1) r_raise("Ruby2D::Ext.begin_frame expects 1 arg (window), got %d", (int)argc);
687
+ R_VAL obj = argv[0];
688
+
689
+ // Sync background color from Ruby to C
690
+ R_VAL bc = r_ivar_get(obj, id_background);
691
+ r2d_window->background.r = NUM2DBL(r_ivar_get(bc, id_r));
692
+ r2d_window->background.g = NUM2DBL(r_ivar_get(bc, id_g));
693
+ r2d_window->background.b = NUM2DBL(r_ivar_get(bc, id_b));
694
+ r2d_window->background.a = NUM2DBL(r_ivar_get(bc, id_a));
695
+
696
+ int was_pending = SDL_SetAtomicInt(&r2d_window->render_pending, 0);
697
+ current_frame_rendered =
698
+ r2d_window->render_mode == R2D_RENDER_CONTINUOUS ||
699
+ was_pending != 0 ||
700
+ r2d_window->screenshot_path != NULL;
701
+
702
+ if (current_frame_rendered) {
703
+ // Count and time only frames actually drawn. In :on_demand mode the tick
704
+ // loop still runs every cycle for input and pacing but presents only when a
705
+ // frame is pending, so frames/fps must advance per present, not per tick.
706
+ // (In :continuous mode every tick renders, so this is unchanged.)
707
+ frames++;
708
+ r2d_window->frames = frames;
709
+ r2d_window->fps = R2D_GetFrameRate();
710
+ r_ivar_set(obj, id_frames, ULL2NUM(r2d_window->frames));
711
+ r_ivar_set(obj, id_fps, DBL2NUM(r2d_window->fps));
712
+
713
+ if (r2d_window->viewport.mode == R2D_VIEWPORT_FIXED) {
714
+ SDL_SetRenderViewport(r2d_window->sdl_renderer, NULL);
715
+ SDL_SetRenderDrawColorFloat(r2d_window->sdl_renderer, 0, 0, 0, 1);
716
+ SDL_RenderClear(r2d_window->sdl_renderer);
717
+ SDL_SetRenderViewport(
718
+ r2d_window->sdl_renderer, &r2d_window->viewport.fixed_rect
719
+ );
720
+ SDL_SetRenderDrawColorFloat(
721
+ r2d_window->sdl_renderer,
722
+ r2d_window->background.r,
723
+ r2d_window->background.g,
724
+ r2d_window->background.b,
725
+ r2d_window->background.a
726
+ );
727
+ SDL_RenderFillRect(r2d_window->sdl_renderer, NULL);
728
+ } else {
729
+ SDL_SetRenderDrawColorFloat(
730
+ r2d_window->sdl_renderer,
731
+ r2d_window->background.r,
732
+ r2d_window->background.g,
733
+ r2d_window->background.b,
734
+ r2d_window->background.a
735
+ );
736
+ SDL_RenderClear(r2d_window->sdl_renderer);
737
+ }
738
+ }
274
739
 
275
- // Swap buffers to display drawn contents in the window
276
- SDL_GL_SwapWindow(window->sdl);
740
+ return current_frame_rendered ? R_TRUE : R_FALSE;
277
741
  }
278
742
 
279
743
 
744
+ /*
745
+ * Ext.end_frame(window)
746
+ *
747
+ * FPS overlay, deferred screenshot, present (if rendered), and frame cap.
748
+ */
749
+ R_VAL ruby2d_ext_window_end_frame(RUBY2D_METHOD_ARGS_VARIADIC) {
750
+ RUBY2D_EXTRACT_VARIADIC;
751
+ (void)argc; (void)argv;
280
752
 
753
+ if (current_frame_rendered) {
754
+ if (r2d_window->diagnostics || r2d_window->show_fps) {
755
+ R2D_DrawFrameRate(r2d_window);
756
+ }
281
757
 
758
+ if (r2d_window->screenshot_path) {
759
+ R2D_Screenshot(r2d_window, r2d_window->screenshot_path);
760
+ free(r2d_window->screenshot_path);
761
+ r2d_window->screenshot_path = NULL;
762
+ }
282
763
 
764
+ if (r2d_window->sdl_renderer) {
765
+ SDL_RenderPresent(r2d_window->sdl_renderer);
766
+ }
767
+ }
283
768
 
769
+ #ifndef __EMSCRIPTEN__
770
+ if (target_frame > 0.0) {
771
+ Uint64 frame_end = SDL_GetPerformanceCounter();
772
+ double elapsed = (double)(frame_end - frame_start) / perf_freq;
773
+ double remaining = target_frame - elapsed;
284
774
 
775
+ if (remaining > 0.0) {
776
+ SDL_DelayPrecise((Uint64)(remaining * 1e9));
777
+ }
778
+ }
779
+ #endif
285
780
 
781
+ return R_NIL;
782
+ }
286
783
 
287
784
 
785
+ // Origin the Ext.now clock is measured from, so it reads zero when a program
786
+ // starts rather than when SDL did. Natively the two amount to the same thing —
787
+ // the engine comes up on first use and lives as long as the process does — so
788
+ // the origin stays 0 and nothing rebases it. The browser is the exception: SDL
789
+ // is initialized once per page load and never quits, while try_run_code starts
790
+ // a new program on every Run. Rebasing there keeps `elapsed` meaning "since
791
+ // this program started" on both, so motion derived from it begins at the
792
+ // beginning rather than picking up wherever the page's uptime had reached.
793
+ static Uint64 r2d_time_origin_ns = 0;
288
794
 
289
795
 
290
796
  /*
291
- * Show the window
797
+ * Ext.now
798
+ *
799
+ * Monotonic, high-resolution time in seconds since the program started. This is
800
+ * the clock the per-frame delta time is measured from. On Emscripten
801
+ * SDL_GetTicksNS maps to performance.now() — the same monotonic high-resolution
802
+ * source the requestAnimationFrame timestamp is drawn from — which is what makes
803
+ * web motion smooth. Time.now (gettimeofday → Date.now on the web) is only ~1 ms
804
+ * and is wall-clock (non-monotonic), so it quantizes and can tick backward,
805
+ * jittering frame pacing. This is the dt clock on mruby (native and web); CRuby
806
+ * uses Process.clock_gettime(CLOCK_MONOTONIC), which is already monotonic.
292
807
  */
293
- int R2D_Show(R2D_Window *win) {
808
+ R_VAL ruby2d_ext_now(RUBY2D_METHOD_ARGS_VARIADIC) {
809
+ RUBY2D_EXTRACT_VARIADIC;
810
+ (void)argc; (void)argv;
811
+ return DBL2NUM((double)(SDL_GetTicksNS() - r2d_time_origin_ns) / 1.0e9);
812
+ }
294
813
 
295
- window = win;
296
814
 
297
- if (!window) {
298
- R2D_Error("R2D_Show", "Window cannot be shown (because it's NULL)");
299
- return 1;
815
+ // =============================================================================
816
+ // Window Show and Initialization
817
+ // =============================================================================
818
+
819
+ #ifdef __EMSCRIPTEN__
820
+ // Web frame pacing. The browser owns the loop via requestAnimationFrame, which
821
+ // always fires at the display refresh rate, so a numeric fps_cap is honored by
822
+ // decimation: EM_TIMING_RAF with interval N runs the tick on every Nth vsync
823
+ // (Emscripten's runner skips the frames in between), giving refresh/N — only
824
+ // divisors of the refresh rate are achievable (120Hz → 60, 40, 30…). N derives
825
+ // from a measured refresh estimate because SDL's Emscripten backend never
826
+ // reports the display's refresh rate. `:infinity` switches to free-running
827
+ // setTimeout(0), the browser's only faster-than-vsync loop, for benchmarking.
828
+ typedef enum {
829
+ WASM_PACE_REFRESH, // no user cap: tick every animation frame (interval 1)
830
+ WASM_PACE_CAPPED, // numeric cap: decimated rAF (interval N)
831
+ WASM_PACE_FREE // :infinity: free-running setTimeout(0)
832
+ } WasmPaceMode;
833
+
834
+ static WasmPaceMode wasm_pace_mode = WASM_PACE_REFRESH;
835
+ static double wasm_pace_cap = 0.0; // target fps when WASM_PACE_CAPPED
836
+ static double wasm_last_tick_time = 0.0; // seconds; 0 = no sample yet
837
+ static double wasm_refresh_est = 0.0; // smoothed rAF rate (Hz); 0 = unknown
838
+ static int wasm_interval = 1; // current decimation interval N
839
+
840
+ // Map the Ruby fps_cap value onto a web pace mode: nil → display refresh,
841
+ // Float::INFINITY → free-running, a number → capped. Called from ext_show and
842
+ // ext_set_fps_cap; the trampoline applies the mode after every tick.
843
+ static void wasm_set_pace_mode(R_VAL fps_cap_val) {
844
+ if (!r_test(fps_cap_val)) {
845
+ wasm_pace_mode = WASM_PACE_REFRESH;
846
+ } else if (isinf(NUM2DBL(fps_cap_val))) {
847
+ wasm_pace_mode = WASM_PACE_FREE;
848
+ } else {
849
+ wasm_pace_mode = WASM_PACE_CAPPED;
850
+ wasm_pace_cap = NUM2DBL(fps_cap_val);
300
851
  }
852
+ }
301
853
 
302
- // Create SDL window
303
- window->sdl = SDL_CreateWindow(
304
- window->title, // title
305
- SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, // window position
306
- window->width, window->height, // window size
307
- SDL_WINDOW_OPENGL | window->flags // flags
308
- );
854
+ static void wasm_tick_trampoline(void) {
855
+ // Release the tick's return value from the GC arena — this call runs outside
856
+ // the VM once per frame, and mrb_funcall arena-protects what it returns, so
857
+ // a heap-object return would otherwise accumulate one pinned object per
858
+ // frame, forever.
859
+ int ai = r_gc_arena_save();
860
+ r_funcall(ruby2d_window, "tick", 0);
861
+ r_gc_arena_restore(ai);
862
+
863
+ // Re-assert the loop timing after every tick, not once: SDL's Emscripten
864
+ // backend defers the renderer's creation-time vsync-0 swap interval
865
+ // (`pending_swap_interval`) and applies it during the first SDL_PumpEvents —
866
+ // mid-tick, after any one-shot pin — silently flipping the loop to
867
+ // free-running setTimeout(0). Pinning after the tick always gets the last
868
+ // word. (It can't run at setup time anyway: emscripten_set_main_loop_timing
869
+ // only takes effect once a main loop is registered.)
870
+
871
+ if (wasm_pace_mode == WASM_PACE_FREE) {
872
+ wasm_last_tick_time = 0.0; // rAF rate can't be sampled from setTimeout ticks
873
+ emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, 0);
874
+ return;
875
+ }
876
+
877
+ // Estimate the raw rAF (display refresh) rate from tick spacing: ticks arrive
878
+ // every `wasm_interval` animation frames, so raw = interval / dt. Samples
879
+ // outside plausible display rates (20–400 Hz) are dropped — the first tick,
880
+ // hidden-tab gaps, and mode switches all land there.
881
+ double now = (double)SDL_GetTicksNS() / 1.0e9;
882
+ if (wasm_last_tick_time > 0.0) {
883
+ double dt = now - wasm_last_tick_time;
884
+ double raw = dt > 0.0 ? (double)wasm_interval / dt : 0.0;
885
+ if (raw >= 20.0 && raw <= 400.0) {
886
+ wasm_refresh_est = wasm_refresh_est > 0.0
887
+ ? wasm_refresh_est * 0.9 + raw * 0.1
888
+ : raw;
889
+ }
890
+ }
891
+ wasm_last_tick_time = now;
892
+
893
+ int interval = 1;
894
+ if (wasm_pace_mode == WASM_PACE_CAPPED &&
895
+ wasm_refresh_est > 0.0 && wasm_pace_cap > 0.0) {
896
+ // Keep the current interval while the ideal ratio stays within ±0.55 of
897
+ // it — hysteresis so estimator noise at a rounding boundary can't
898
+ // oscillate N (visibly flipping the frame rate) between adjacent values.
899
+ double ratio = wasm_refresh_est / wasm_pace_cap;
900
+ interval = wasm_interval;
901
+ if (fabs(ratio - (double)wasm_interval) > 0.55) {
902
+ interval = (int)SDL_round(ratio);
903
+ if (interval < 1) interval = 1;
904
+ }
905
+ }
906
+ wasm_interval = interval;
907
+ emscripten_set_main_loop_timing(EM_TIMING_RAF, interval);
908
+ }
909
+ #endif
309
910
 
310
- if (!window->sdl) R2D_Error("SDL_CreateWindow", SDL_GetError());
311
- if (window->icon) R2D_SetIcon(window, window->icon);
312
911
 
313
- // The window created by SDL might not actually be the requested size.
314
- // If it's not the same, retrieve and store the actual window size.
315
- int actual_width, actual_height;
316
- SDL_GetWindowSize(window->sdl, &actual_width, &actual_height);
912
+ /*
913
+ * Ruby2D::Window#ext_show
914
+ */
915
+ R_VAL ruby2d_ext_window_show(RUBY2D_METHOD_ARGS_VARIADIC) {
916
+ RUBY2D_EXTRACT_VARIADIC;
917
+ if (argc != 1) r_raise("Ruby2D::Ext.window_show expects 1 arg (window), got %d", (int)argc);
918
+ R_VAL obj = argv[0];
317
919
 
318
- if ((window->width != actual_width) || (window->height != actual_height)) {
319
- R2D_Log(R2D_INFO,
320
- "Scaling window to %ix%i (requested size was %ix%i)",
321
- actual_width, actual_height, window->width, window->height
322
- );
323
- window->width = actual_width;
324
- window->height = actual_height;
325
- window->orig_width = actual_width;
326
- window->orig_height = actual_height;
920
+ ruby2d_window = obj;
921
+
922
+ if (!r2d_window) {
923
+ r_raise("Ruby2D: window cannot be shown (native window is NULL)");
327
924
  }
328
925
 
329
- // Set Up OpenGL /////////////////////////////////////////////////////////////
926
+ // Re-read window flags from Ruby (user may have changed @resizable,
927
+ // @highdpi via set() after ext_create but before show)
928
+ int flags = 0;
929
+ if (r_test(r_ivar_get(obj, id_resizable))) flags |= R2D_RESIZABLE;
930
+ if (r_test(r_ivar_get(obj, id_highdpi))) flags |= R2D_HIGHDPI;
931
+ r2d_window->flags = flags;
932
+
933
+ // Scale window dimensions by content_scale for correct visual size on all
934
+ // platforms. On macOS content_scale is 1.0 (coordinates are already logical
935
+ // points); on Windows with DPI scaling it compensates for physical pixels.
936
+ float cs = r2d_window->display_content_scale;
937
+ int win_w = (int)(obj_int(obj, id_width) * cs);
938
+ int win_h = (int)(obj_int(obj, id_height) * cs);
939
+
940
+ // Create the window hidden on native platforms so it can be centered before
941
+ // its first paint; repositioning an already-visible window makes it visibly
942
+ // jump. On the web the "window" is the page canvas — there is nothing to
943
+ // position — so it is created shown, as before.
944
+ int create_flags = r2d_window->flags;
945
+ #ifndef __EMSCRIPTEN__
946
+ create_flags |= SDL_WINDOW_HIDDEN;
947
+ #endif
330
948
 
331
- R2D_GL_Init(window);
949
+ // Create SDL window and renderer
950
+ if (!SDL_CreateWindowAndRenderer(
951
+ obj_str(obj, id_title), win_w, win_h,
952
+ create_flags,
953
+ &r2d_window->sdl_window, &r2d_window->sdl_renderer
954
+ )) {
955
+ r_raise("Ruby2D: failed to create window and renderer: %s", SDL_GetError());
956
+ }
332
957
 
333
- // SDL 2.0.10 and macOS 10.15 fix ////////////////////////////////////////////
958
+ // Defensive check: ensure both window and renderer are non-NULL
959
+ if (!r2d_window->sdl_window || !r2d_window->sdl_renderer) {
960
+ r_raise("Ruby2D: window or renderer is NULL after creation: %s", SDL_GetError());
961
+ }
334
962
 
335
- #if MACOS
336
- SDL_SetWindowSize(window->sdl, window->width, window->height);
963
+ // Enable alpha blending for shape rendering (SDL_RenderGeometry)
964
+ SDL_SetRenderDrawBlendMode(r2d_window->sdl_renderer, SDL_BLENDMODE_BLEND);
965
+
966
+ // Resolve the effective fps_cap. Float::INFINITY means "uncapped" → fps_cap 0,
967
+ // so target_frame stays 0 (no frame delay). Read as a double so an Integer or
968
+ // Float::INFINITY both work. Without a user cap, fall back to the display
969
+ // refresh rate.
970
+ R_VAL fps_cap_val = r_ivar_get(obj, id_fps_cap);
971
+ if (r_test(fps_cap_val)) {
972
+ double cap = NUM2DBL(fps_cap_val);
973
+ r2d_window->fps_cap = isinf(cap) ? 0 : (int)cap;
974
+ } else {
975
+ r2d_window->fps_cap = (int)r2d_window->display_refresh_rate;
976
+ }
977
+
978
+ // On the web, pacing is applied by the tick trampoline instead of vsync or
979
+ // frame delays — map the cap onto a pace mode for it.
980
+ #ifdef __EMSCRIPTEN__
981
+ wasm_set_pace_mode(fps_cap_val);
337
982
  #endif
338
983
 
339
- // Set Main Loop Data ////////////////////////////////////////////////////////
984
+ #ifndef __EMSCRIPTEN__
985
+ // Native frame pacing: enable vsync for smooth display-synced rendering. A user
986
+ // cap disables vsync and paces solely via SDL_DelayPrecise; without a cap, use
987
+ // vsync AND a refresh-rate SDL_DelayPrecise fallback — this prevents uncapped
988
+ // rendering when the window is occluded, where macOS (and other platforms) may
989
+ // not block on vsync.
990
+ //
991
+ // On the web this is deliberately skipped. With no ASYNCIFY the renderer present
992
+ // can't block, so SDL's vsync setter does nothing but rewrite the Emscripten
993
+ // main-loop timing (vsync 0 → free-running EM_TIMING_SETTIMEOUT, vsync 1 → an
994
+ // EM_TIMING_RAF cadence that measured at half the refresh rate). We own that
995
+ // timing directly instead — see emscripten_set_main_loop_timing below.
996
+ SDL_SetRenderVSync(r2d_window->sdl_renderer, r_test(fps_cap_val) ? 0 : 1);
997
+ #endif
998
+
999
+ // Re-read the logical (user-intended) viewport dimensions from Ruby (the user
1000
+ // may have changed @width/@height or @viewport_* via set() after ext_create
1001
+ // but before show). These are the unscaled base sizes; R2D_ApplyPixelScale
1002
+ // derives the render dimensions from them.
1003
+ R_VAL vw_val = r_ivar_get(obj, id_viewport_width);
1004
+ R_VAL vh_val = r_ivar_get(obj, id_viewport_height);
1005
+ r2d_window->viewport.base_width = r_test(vw_val) ? NUM2INT(vw_val) : obj_int(obj, id_width);
1006
+ r2d_window->viewport.base_height = r_test(vh_val) ? NUM2INT(vh_val) : obj_int(obj, id_height);
1007
+
1008
+ // Re-read viewport mode from Ruby
1009
+ r2d_window->viewport.mode = R2D_ParseViewportMode(r_ivar_get(obj, id_viewport_mode));
1010
+
1011
+ // Re-read render mode from Ruby (user may have changed @render_mode via set()
1012
+ // between new and show)
1013
+ r2d_window->render_mode = R2D_ParseRenderMode(r_ivar_get(obj, id_render_mode));
1014
+
1015
+ // Logical (user-intended) window dimensions on the C struct
1016
+ r2d_window->orig_width = obj_int(obj, id_width);
1017
+ r2d_window->orig_height = obj_int(obj, id_height);
1018
+
1019
+ // Derive render (physical) dimensions from the logical sizes, scaling once
1020
+ // for pixel_scale on HiDPI so coordinates map 1:1 with physical pixels.
1021
+ r2d_window->pixel_scale = obj_bool(obj, id_pixel_scale);
1022
+ R2D_ApplyPixelScale(r2d_window);
1023
+
1024
+ // Apply viewport scaling mode (logical presentation)
1025
+ R2D_ApplyViewportMode(r2d_window);
1026
+
1027
+ // Read icon path from Ruby and apply it
1028
+ R_VAL icon_val = r_ivar_get(obj, id_icon);
1029
+ if (r_test(icon_val)) {
1030
+ R2D_SetIcon(r2d_window, RSTRING_PTR(icon_val));
1031
+ }
340
1032
 
1033
+ // Reset first_poll so the initial gamepad scan runs on the first tick
1034
+ first_poll = true;
1035
+
1036
+ // Set Main Loop Data ////////////////////////////////////////////////////////
341
1037
 
342
1038
  frames = 0; // Total frames since start
343
- frames_last_sec = 0; // Frames in the last second
344
- start_ms = SDL_GetTicks(); // Elapsed time since start
345
- next_second_ms = SDL_GetTicks(); // The last time plus a second
346
- begin_ms = start_ms; // Time at beginning of loop
347
- decay_rate = 0.5; // Determines how fast an average decays over time
348
- fps = window->fps_cap; // Moving average of actual FPS, initial value a guess
349
-
350
- // Enable VSync
351
- if (window->vsync) {
352
- if (!SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1")) {
353
- R2D_Log(R2D_WARN, "VSync cannot be enabled");
354
- }
355
- }
356
1039
 
357
- window->close = false;
1040
+ r2d_window->close = false;
1041
+
1042
+ // Frame cap setup: always set a target_frame based on fps_cap. Without an
1043
+ // explicit user cap, this equals the display refresh rate (set above), acting
1044
+ // as a fallback when vsync is not blocking (e.g., window occluded).
1045
+ perf_freq = SDL_GetPerformanceFrequency();
1046
+ target_frame = r2d_window->fps_cap > 0
1047
+ ? 1.0 / (double)r2d_window->fps_cap
1048
+ : 0.0;
1049
+
1050
+ // Center the window on the primary display and reveal it. It was created
1051
+ // hidden above, so this positions it before its first paint — no visible
1052
+ // jump. Explicit centering is honored on all native backends, unlike the
1053
+ // default undefined position (which lands top-left under many Linux WMs).
1054
+ // Skipped on the web, where the canvas has no OS-level position.
1055
+ #ifndef __EMSCRIPTEN__
1056
+ SDL_SetWindowPosition(r2d_window->sdl_window,
1057
+ SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
1058
+ SDL_ShowWindow(r2d_window->sdl_window);
1059
+ #endif
358
1060
 
359
1061
  // Main Loop /////////////////////////////////////////////////////////////////
1062
+ // CRuby: return and let Ruby own the loop (Window#show calls tick in a loop).
1063
+ // mruby native: C drives the loop, calling Ruby's tick each frame.
1064
+ // WASM: emscripten_set_main_loop calls tick via a C trampoline.
360
1065
 
361
1066
  #ifdef __EMSCRIPTEN__
362
- emscripten_set_main_loop(main_loop, 0, true);
363
- #else
364
- while (!window->close) main_loop();
1067
+ emscripten_set_main_loop(wasm_tick_trampoline, 0, true);
1068
+ #elif defined(MRUBY)
1069
+ // Arena save/restore for the same reason as wasm_tick_trampoline: each
1070
+ // C-driven tick would otherwise pin its return value in the GC arena.
1071
+ while (!r2d_window->close) {
1072
+ int ai = r_gc_arena_save();
1073
+ r_funcall(ruby2d_window, "tick", 0);
1074
+ r_gc_arena_restore(ai);
1075
+ }
365
1076
  #endif
366
1077
 
367
- return 0;
1078
+ return R_TRUE;
368
1079
  }
369
1080
 
370
1081
 
1082
+ // =============================================================================
1083
+ // Window Operations
1084
+ // =============================================================================
371
1085
 
372
1086
  /*
373
1087
  * Set the icon for the window
374
1088
  */
375
1089
  void R2D_SetIcon(R2D_Window *window, const char *icon) {
376
- SDL_Surface *iconSurface = R2D_CreateImageSurface(icon);
1090
+ SDL_Surface *iconSurface = IMG_Load(icon);
377
1091
  if (iconSurface) {
378
1092
  window->icon = icon;
379
- SDL_SetWindowIcon(window->sdl, iconSurface);
380
- SDL_FreeSurface(iconSurface);
1093
+ SDL_SetWindowIcon(window->sdl_window, iconSurface);
1094
+ SDL_DestroySurface(iconSurface);
381
1095
  } else {
382
- R2D_Log(R2D_WARN, "Could not set window icon");
1096
+ R2D_Log(R2D_WARN, "Could not set window icon: %s", SDL_GetError());
383
1097
  }
384
1098
  }
385
1099
 
@@ -388,48 +1102,106 @@ void R2D_SetIcon(R2D_Window *window, const char *icon) {
388
1102
  * Take a screenshot of the window
389
1103
  */
390
1104
  void R2D_Screenshot(R2D_Window *window, const char *path) {
1105
+ SDL_Surface *surface = SDL_RenderReadPixels(window->sdl_renderer, NULL);
1106
+ if (!surface) {
1107
+ R2D_Error("R2D_Screenshot", "%s", SDL_GetError());
1108
+ return;
1109
+ }
391
1110
 
392
- #if GLES
393
- R2D_Error("R2D_Screenshot", "Not supported in OpenGL ES");
394
- #else
395
- // Create a surface the size of the window
396
- SDL_Surface *surface = SDL_CreateRGBSurface(
397
- SDL_SWSURFACE, window->width, window->height, 24,
398
- 0x000000FF, 0x0000FF00, 0x00FF0000, 0
399
- );
1111
+ if (!IMG_SavePNG(surface, path)) {
1112
+ R2D_Error("R2D_Screenshot", "%s", SDL_GetError());
1113
+ }
400
1114
 
401
- // Grab the pixels from the front buffer, save to surface
402
- glReadBuffer(GL_FRONT);
403
- glReadPixels(0, 0, window->width, window->height, GL_RGB, GL_UNSIGNED_BYTE, surface->pixels);
1115
+ SDL_DestroySurface(surface);
1116
+ }
404
1117
 
405
- // Flip image vertically
406
1118
 
407
- void *temp_row = (void *)malloc(surface->pitch);
408
- if (!temp_row) {
409
- R2D_Error("R2D_Screenshot", "Out of memory!");
410
- SDL_FreeSurface(surface);
411
- return;
412
- }
1119
+ /*
1120
+ * Show the cursor over the window
1121
+ */
1122
+ void R2D_ShowCursor() {
1123
+ SDL_ShowCursor();
1124
+ }
413
1125
 
414
- int height_div_2 = (int) (surface->h * 0.5);
415
1126
 
416
- for (int index = 0; index < height_div_2; index++) {
417
- memcpy((Uint8 *)temp_row,(Uint8 *)(surface->pixels) + surface->pitch * index, surface->pitch);
418
- memcpy((Uint8 *)(surface->pixels) + surface->pitch * index, (Uint8 *)(surface->pixels) + surface->pitch * (surface->h - index-1), surface->pitch);
419
- memcpy((Uint8 *)(surface->pixels) + surface->pitch * (surface->h - index-1), temp_row, surface->pitch);
420
- }
1127
+ /*
1128
+ * Hide the cursor over the window
1129
+ */
1130
+ void R2D_HideCursor() {
1131
+ SDL_HideCursor();
1132
+ }
421
1133
 
422
- free(temp_row);
423
1134
 
424
- // Save image to disk
425
- IMG_SavePNG(surface, path);
426
- SDL_FreeSurface(surface);
427
- #endif
1135
+ /*
1136
+ * Check if the cursor is visible
1137
+ */
1138
+ bool R2D_CursorVisible() {
1139
+ return SDL_CursorVisible();
1140
+ }
1141
+
1142
+
1143
+ /*
1144
+ * Set the system cursor by name
1145
+ */
1146
+ bool R2D_SetSystemCursor(const char *name) {
1147
+ SDL_SystemCursor id;
1148
+
1149
+ if (strcmp(name, "default") == 0) id = SDL_SYSTEM_CURSOR_DEFAULT;
1150
+ else if (strcmp(name, "text") == 0) id = SDL_SYSTEM_CURSOR_TEXT;
1151
+ else if (strcmp(name, "wait") == 0) id = SDL_SYSTEM_CURSOR_WAIT;
1152
+ else if (strcmp(name, "crosshair") == 0) id = SDL_SYSTEM_CURSOR_CROSSHAIR;
1153
+ else if (strcmp(name, "progress") == 0) id = SDL_SYSTEM_CURSOR_PROGRESS;
1154
+ else if (strcmp(name, "nwse_resize") == 0) id = SDL_SYSTEM_CURSOR_NWSE_RESIZE;
1155
+ else if (strcmp(name, "nesw_resize") == 0) id = SDL_SYSTEM_CURSOR_NESW_RESIZE;
1156
+ else if (strcmp(name, "ew_resize") == 0) id = SDL_SYSTEM_CURSOR_EW_RESIZE;
1157
+ else if (strcmp(name, "ns_resize") == 0) id = SDL_SYSTEM_CURSOR_NS_RESIZE;
1158
+ else if (strcmp(name, "move") == 0) id = SDL_SYSTEM_CURSOR_MOVE;
1159
+ else if (strcmp(name, "not_allowed") == 0) id = SDL_SYSTEM_CURSOR_NOT_ALLOWED;
1160
+ else if (strcmp(name, "pointer") == 0) id = SDL_SYSTEM_CURSOR_POINTER;
1161
+ else if (strcmp(name, "nw_resize") == 0) id = SDL_SYSTEM_CURSOR_NW_RESIZE;
1162
+ else if (strcmp(name, "n_resize") == 0) id = SDL_SYSTEM_CURSOR_N_RESIZE;
1163
+ else if (strcmp(name, "ne_resize") == 0) id = SDL_SYSTEM_CURSOR_NE_RESIZE;
1164
+ else if (strcmp(name, "e_resize") == 0) id = SDL_SYSTEM_CURSOR_E_RESIZE;
1165
+ else if (strcmp(name, "se_resize") == 0) id = SDL_SYSTEM_CURSOR_SE_RESIZE;
1166
+ else if (strcmp(name, "s_resize") == 0) id = SDL_SYSTEM_CURSOR_S_RESIZE;
1167
+ else if (strcmp(name, "sw_resize") == 0) id = SDL_SYSTEM_CURSOR_SW_RESIZE;
1168
+ else if (strcmp(name, "w_resize") == 0) id = SDL_SYSTEM_CURSOR_W_RESIZE;
1169
+ else {
1170
+ R2D_Error("R2D_SetSystemCursor", "Unknown cursor name: %s", name);
1171
+ return false;
1172
+ }
1173
+
1174
+ // Cache one cursor per system-cursor id so repeated sets don't leak a fresh
1175
+ // SDL_Cursor each call (SDL_SetCursor doesn't take ownership). The cache lives
1176
+ // for the process — bounded by SDL_SYSTEM_CURSOR_COUNT, matching the
1177
+ // single-window/no-teardown design.
1178
+ static SDL_Cursor *cursor_cache[SDL_SYSTEM_CURSOR_COUNT] = { NULL };
1179
+
1180
+ SDL_Cursor *cursor = cursor_cache[id];
1181
+ if (!cursor) {
1182
+ cursor = SDL_CreateSystemCursor(id);
1183
+ if (!cursor) {
1184
+ R2D_Error("R2D_SetSystemCursor", "Failed to create cursor: %s", SDL_GetError());
1185
+ return false;
1186
+ }
1187
+ cursor_cache[id] = cursor;
1188
+ }
1189
+
1190
+ SDL_SetCursor(cursor);
1191
+ SDL_ShowCursor();
1192
+ return true;
428
1193
  }
429
1194
 
430
1195
 
431
1196
  /*
432
1197
  * Close the window
1198
+ *
1199
+ * This only flags the loop to stop; it does NOT destroy the SDL window or
1200
+ * renderer. Those are torn down in R2D_FreeWindow, which runs from the GC
1201
+ * finalizer. The deferral is deliberate: the window object is pinned by
1202
+ * DSL.window for the life of the process, so a closed window can be re-shown,
1203
+ * and destroying SDL resources here would double-free when the finalizer later
1204
+ * runs over the same pointers.
433
1205
  */
434
1206
  int R2D_Close(R2D_Window *window) {
435
1207
  if (!window->close) {
@@ -440,13 +1212,651 @@ int R2D_Close(R2D_Window *window) {
440
1212
  }
441
1213
 
442
1214
 
1215
+ /*
1216
+ * Whether the renderer is still alive. Texture finalizers consult this: at
1217
+ * interpreter shutdown Ruby may free the Window — destroying the renderer and,
1218
+ * with it, every texture created from it — before an Image/Text/Canvas/
1219
+ * BitmapText finalizer runs, so calling SDL_DestroyTexture afterward would
1220
+ * double-free. R2D_FreeWindow clears r2d_window, so a NULL window means gone.
1221
+ */
1222
+ bool R2D_RendererAlive(void) {
1223
+ R2D_Window *w = R2D_GetWindow();
1224
+ return w && w->sdl_renderer;
1225
+ }
1226
+
1227
+
443
1228
  /*
444
1229
  * Free all resources
445
1230
  */
446
1231
  int R2D_FreeWindow(R2D_Window *window) {
447
1232
  R2D_Close(window);
448
- SDL_GL_DeleteContext(window->glcontext);
449
- SDL_DestroyWindow(window->sdl);
1233
+ R2D_FreeFpsOverlay();
1234
+ if (window->sdl_renderer) {
1235
+ SDL_DestroyRenderer(window->sdl_renderer);
1236
+ window->sdl_renderer = NULL;
1237
+ }
1238
+ if (window->sdl_window) {
1239
+ SDL_DestroyWindow(window->sdl_window);
1240
+ window->sdl_window = NULL;
1241
+ }
1242
+ free(window->screenshot_path);
1243
+ if (r2d_window == window) r2d_window = NULL;
450
1244
  free(window);
451
1245
  return 0;
452
1246
  }
1247
+
1248
+
1249
+ /*
1250
+ * Free callback for Ruby's typed data (matches R2D_DEFINE_DATA_TYPE)
1251
+ */
1252
+ static void R2D_Window_free(void *p) {
1253
+ if (!p) return;
1254
+ R2D_FreeWindow((R2D_Window *)p);
1255
+ }
1256
+
1257
+
1258
+ // =============================================================================
1259
+ // Ruby Method Bindings
1260
+ // =============================================================================
1261
+
1262
+ /*
1263
+ * Ruby2D::Window#ext_get_display_dimensions
1264
+ */
1265
+ R_VAL ruby2d_ext_window_get_display_dimensions(RUBY2D_METHOD_ARGS_VARIADIC) {
1266
+ RUBY2D_EXTRACT_VARIADIC;
1267
+ if (argc != 1) r_raise("Ruby2D::Ext.window_get_display_dimensions expects 1 arg (window), got %d", (int)argc);
1268
+ R_VAL obj = argv[0];
1269
+ int w; int h;
1270
+ R2D_GetDisplayDimensions(&w, &h);
1271
+ r_iv_set(obj, "@display_width", INT2NUM(w));
1272
+ r_iv_set(obj, "@display_height", INT2NUM(h));
1273
+ r_iv_set(obj, "@display_pixel_width", INT2NUM(r2d_window->display_pixel_width));
1274
+ r_iv_set(obj, "@display_pixel_height", INT2NUM(r2d_window->display_pixel_height));
1275
+ return R_NIL;
1276
+ }
1277
+
1278
+
1279
+ /*
1280
+ * Ruby2D::Window#ext_set_viewport_mode
1281
+ *
1282
+ * Reads @viewport_mode, @viewport_width, @viewport_height from Ruby and
1283
+ * applies the new viewport mode at runtime.
1284
+ */
1285
+ R_VAL ruby2d_ext_window_set_viewport_mode(RUBY2D_METHOD_ARGS_VARIADIC) {
1286
+ RUBY2D_EXTRACT_VARIADIC;
1287
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_viewport_mode expects 1 arg (window), got %d", (int)argc);
1288
+ R_VAL obj = argv[0];
1289
+ if (!r2d_window) return R_FALSE;
1290
+
1291
+ // Detect an explicit viewport-dimension change from Ruby. @viewport_* is
1292
+ // synced to the current (possibly pixel-scaled) render viewport each frame, so
1293
+ // a value that differs means the user set a new size via set() — adopt it as
1294
+ // the logical base. A matching value (the common case) leaves the base alone,
1295
+ // which is what keeps repeated set() calls from compounding.
1296
+ R_VAL vw_val = r_ivar_get(obj, id_viewport_width);
1297
+ R_VAL vh_val = r_ivar_get(obj, id_viewport_height);
1298
+ int ruby_vw = r_test(vw_val) ? NUM2INT(vw_val) : r2d_window->width;
1299
+ int ruby_vh = r_test(vh_val) ? NUM2INT(vh_val) : r2d_window->height;
1300
+
1301
+ if (ruby_vw != r2d_window->viewport.width ||
1302
+ ruby_vh != r2d_window->viewport.height) {
1303
+ r2d_window->viewport.base_width = ruby_vw;
1304
+ r2d_window->viewport.base_height = ruby_vh;
1305
+ }
1306
+
1307
+ // Parse the (possibly new) viewport mode
1308
+ r2d_window->viewport.mode = R2D_ParseViewportMode(r_ivar_get(obj, id_viewport_mode));
1309
+
1310
+ // Re-derive the render dimensions from the logical base. Idempotent, so
1311
+ // toggling pixel_scale or changing viewport mode at runtime no longer doubles
1312
+ // the window/viewport size on each call.
1313
+ r2d_window->pixel_scale = obj_bool(obj, id_pixel_scale);
1314
+ R2D_ApplyPixelScale(r2d_window);
1315
+
1316
+ R2D_ApplyViewportMode(r2d_window);
1317
+
1318
+ return R_TRUE;
1319
+ }
1320
+
1321
+
1322
+ /*
1323
+ * Ruby2D::Window#ext_set_icon
1324
+ */
1325
+ R_VAL ruby2d_ext_window_set_icon(RUBY2D_METHOD_ARGS_VARIADIC) {
1326
+ RUBY2D_EXTRACT_VARIADIC;
1327
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_icon expects 1 arg (window), got %d", (int)argc);
1328
+ R_VAL obj = argv[0];
1329
+ if (!r2d_window || !r2d_window->sdl_window) return R_FALSE;
1330
+
1331
+ R_VAL icon_val = r_ivar_get(obj, id_icon);
1332
+ if (r_test(icon_val)) {
1333
+ R2D_SetIcon(r2d_window, RSTRING_PTR(icon_val));
1334
+ }
1335
+ return R_TRUE;
1336
+ }
1337
+
1338
+
1339
+ /*
1340
+ * Ruby2D::Window#ext_set_title — push @title to the live window's title bar
1341
+ */
1342
+ R_VAL ruby2d_ext_window_set_title(RUBY2D_METHOD_ARGS_VARIADIC) {
1343
+ RUBY2D_EXTRACT_VARIADIC;
1344
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_title expects 1 arg (window), got %d", (int)argc);
1345
+ R_VAL obj = argv[0];
1346
+ if (!r2d_window || !r2d_window->sdl_window) return R_FALSE;
1347
+
1348
+ SDL_SetWindowTitle(r2d_window->sdl_window, obj_str(obj, id_title));
1349
+ return R_TRUE;
1350
+ }
1351
+
1352
+
1353
+ /*
1354
+ * Ruby2D::Window#ext_set_resizable — toggle the live window's resizability
1355
+ */
1356
+ R_VAL ruby2d_ext_window_set_resizable(RUBY2D_METHOD_ARGS_VARIADIC) {
1357
+ RUBY2D_EXTRACT_VARIADIC;
1358
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_resizable expects 1 arg (window), got %d", (int)argc);
1359
+ R_VAL obj = argv[0];
1360
+ if (!r2d_window || !r2d_window->sdl_window) return R_FALSE;
1361
+
1362
+ bool resizable = r_test(r_ivar_get(obj, id_resizable));
1363
+ SDL_SetWindowResizable(r2d_window->sdl_window, resizable);
1364
+ if (resizable) r2d_window->flags |= R2D_RESIZABLE;
1365
+ else r2d_window->flags &= ~R2D_RESIZABLE;
1366
+ return R_TRUE;
1367
+ }
1368
+
1369
+
1370
+ /*
1371
+ * Ruby2D::Window#ext_set_size — resize the live window to the logical @width/
1372
+ * @height, then re-derive the render/viewport dimensions (mirrors the resize-
1373
+ * event path). The window manager may honor a different size; the resulting
1374
+ * SDL_EVENT_WINDOW_RESIZED reconciles orig_width/orig_height if so.
1375
+ */
1376
+ R_VAL ruby2d_ext_window_set_size(RUBY2D_METHOD_ARGS_VARIADIC) {
1377
+ RUBY2D_EXTRACT_VARIADIC;
1378
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_size expects 1 arg (window), got %d", (int)argc);
1379
+ R_VAL obj = argv[0];
1380
+ if (!r2d_window || !r2d_window->sdl_window) return R_FALSE;
1381
+
1382
+ r2d_window->orig_width = obj_int(obj, id_width);
1383
+ r2d_window->orig_height = obj_int(obj, id_height);
1384
+
1385
+ // In expand mode the viewport tracks the window size; fixed/scaled modes keep
1386
+ // their own logical canvas (viewport.base_*).
1387
+ if (r2d_window->viewport.mode == R2D_VIEWPORT_EXPAND) {
1388
+ r2d_window->viewport.base_width = r2d_window->orig_width;
1389
+ r2d_window->viewport.base_height = r2d_window->orig_height;
1390
+ }
1391
+
1392
+ // SDL_CreateWindowAndRenderer was given logical * content_scale at show time;
1393
+ // match that so the requested logical size maps to the same physical pixels.
1394
+ float cs = r2d_window->display_content_scale;
1395
+ if (!SDL_SetWindowSize(r2d_window->sdl_window,
1396
+ (int)(r2d_window->orig_width * cs),
1397
+ (int)(r2d_window->orig_height * cs))) {
1398
+ R2D_Error("SDL_SetWindowSize", "%s", SDL_GetError());
1399
+ }
1400
+
1401
+ R2D_ApplyPixelScale(r2d_window);
1402
+ R2D_ApplyViewportMode(r2d_window);
1403
+ return R_TRUE;
1404
+ }
1405
+
1406
+
1407
+ /*
1408
+ * Ruby2D::Window#ext_diagnostics
1409
+ */
1410
+ R_VAL ruby2d_ext_window_diagnostics(RUBY2D_METHOD_ARGS_VARIADIC) {
1411
+ RUBY2D_EXTRACT_VARIADIC;
1412
+ if (argc != 2) r_raise("Ruby2D::Ext.window_diagnostics expects 2 args, got %d", (int)argc);
1413
+ R_VAL enable = argv[1];
1414
+ // Set Ruby 2D native diagnostics
1415
+ R2D_Diagnostics(r_test(enable));
1416
+ return R_TRUE;
1417
+ }
1418
+
1419
+
1420
+ /*
1421
+ * Ruby2D::Window#ext_set_fps_cap
1422
+ */
1423
+ R_VAL ruby2d_ext_window_set_fps_cap(RUBY2D_METHOD_ARGS_VARIADIC) {
1424
+ RUBY2D_EXTRACT_VARIADIC;
1425
+ if (argc != 2) r_raise("Ruby2D::Ext.window_set_fps_cap expects 2 args, got %d", (int)argc);
1426
+ R_VAL fps_cap = argv[1];
1427
+ if (!r2d_window || !r2d_window->sdl_renderer) return R_FALSE;
1428
+
1429
+ if (!r_test(fps_cap)) {
1430
+ // nil → no cap: restore vsync-driven pacing with the refresh-rate fallback.
1431
+ r2d_window->fps_cap = (int)r2d_window->display_refresh_rate;
1432
+ #ifndef __EMSCRIPTEN__
1433
+ SDL_SetRenderVSync(r2d_window->sdl_renderer, 1);
1434
+ #endif
1435
+ } else {
1436
+ // A finite cap paces via SDL_DelayPrecise; Float::INFINITY → uncapped.
1437
+ // (The Ruby side has already rejected 0/negative/NaN.)
1438
+ double cap = NUM2DBL(fps_cap);
1439
+ r2d_window->fps_cap = isinf(cap) ? 0 : (int)cap;
1440
+ #ifndef __EMSCRIPTEN__
1441
+ SDL_SetRenderVSync(r2d_window->sdl_renderer, 0);
1442
+ #endif
1443
+ }
1444
+ // On the web, vsync isn't touched: the requestAnimationFrame main loop owns the
1445
+ // cadence, and SDL's vsync setter would only rewrite that timing (and SDL_Delay-
1446
+ // Precise pacing is compiled out under Emscripten). Pacing is applied by the
1447
+ // tick trampoline instead — see wasm_set_pace_mode / wasm_tick_trampoline.
1448
+ #ifdef __EMSCRIPTEN__
1449
+ wasm_set_pace_mode(fps_cap);
1450
+ #endif
1451
+ target_frame = r2d_window->fps_cap > 0 ? 1.0 / (double)r2d_window->fps_cap : 0.0;
1452
+ return R_TRUE;
1453
+ }
1454
+
1455
+
1456
+ /*
1457
+ * Ruby2D::Window#ext_show_fps
1458
+ */
1459
+ R_VAL ruby2d_ext_window_show_fps(RUBY2D_METHOD_ARGS_VARIADIC) {
1460
+ RUBY2D_EXTRACT_VARIADIC;
1461
+ if (argc != 2) r_raise("Ruby2D::Ext.window_show_fps expects 2 args, got %d", (int)argc);
1462
+ R_VAL enable = argv[1];
1463
+ R2D_ShowFPS(r_test(enable));
1464
+ return R_TRUE;
1465
+ }
1466
+
1467
+
1468
+ /*
1469
+ * Ruby2D::Window#ext_load_gamepad_mappings_file
1470
+ */
1471
+ R_VAL ruby2d_ext_window_load_gamepad_mappings_file(RUBY2D_METHOD_ARGS_VARIADIC) {
1472
+ RUBY2D_EXTRACT_VARIADIC;
1473
+ if (argc != 2) r_raise("Ruby2D::Ext.window_load_gamepad_mappings_file expects 2 args, got %d", (int)argc);
1474
+ R_VAL path = argv[1];
1475
+ R2D_Log(R2D_INFO, "Loading gamepad mappings from `%s`", RSTRING_PTR(path));
1476
+ int n = R2D_AddGamepadMappingsFromFile(RSTRING_PTR(path));
1477
+ return INT2NUM(n);
1478
+ }
1479
+
1480
+
1481
+ /*
1482
+ * Ruby2D::Window#ext_add_gamepad_mapping
1483
+ */
1484
+ R_VAL ruby2d_ext_window_add_gamepad_mapping(RUBY2D_METHOD_ARGS_VARIADIC) {
1485
+ RUBY2D_EXTRACT_VARIADIC;
1486
+ if (argc != 2) r_raise("Ruby2D::Ext.window_add_gamepad_mapping expects 2 args, got %d", (int)argc);
1487
+ R_VAL mapping = argv[1];
1488
+ int n = R2D_AddGamepadMapping(RSTRING_PTR(mapping));
1489
+ return INT2NUM(n);
1490
+ }
1491
+
1492
+
1493
+ /*
1494
+ * Ruby2D::Window#ext_gamepad_rumble
1495
+ */
1496
+ R_VAL ruby2d_ext_window_gamepad_rumble(RUBY2D_METHOD_ARGS_VARIADIC) {
1497
+ RUBY2D_EXTRACT_VARIADIC;
1498
+ if (argc != 5) r_raise("Ruby2D::Ext.window_gamepad_rumble expects 5 args, got %d", (int)argc);
1499
+ R_VAL id = argv[1];
1500
+ R_VAL low = argv[2];
1501
+ R_VAL high = argv[3];
1502
+ R_VAL duration = argv[4];
1503
+ bool ok = R2D_RumbleGamepad(
1504
+ (SDL_JoystickID)NUM2INT(id),
1505
+ NUM2DBL(low), NUM2DBL(high), NUM2DBL(duration)
1506
+ );
1507
+ return ok ? R_TRUE : R_FALSE;
1508
+ }
1509
+
1510
+
1511
+ /*
1512
+ * Ruby2D::Window#ext_gamepad_rumble_triggers
1513
+ */
1514
+ R_VAL ruby2d_ext_window_gamepad_rumble_triggers(RUBY2D_METHOD_ARGS_VARIADIC) {
1515
+ RUBY2D_EXTRACT_VARIADIC;
1516
+ if (argc != 5) r_raise("Ruby2D::Ext.window_gamepad_rumble_triggers expects 5 args, got %d", (int)argc);
1517
+ R_VAL id = argv[1];
1518
+ R_VAL left = argv[2];
1519
+ R_VAL right = argv[3];
1520
+ R_VAL duration = argv[4];
1521
+ bool ok = R2D_RumbleGamepadTriggers(
1522
+ (SDL_JoystickID)NUM2INT(id),
1523
+ NUM2DBL(left), NUM2DBL(right), NUM2DBL(duration)
1524
+ );
1525
+ return ok ? R_TRUE : R_FALSE;
1526
+ }
1527
+
1528
+
1529
+ /*
1530
+ * Ruby2D::Window#ext_gamepad_set_led
1531
+ */
1532
+ R_VAL ruby2d_ext_window_gamepad_set_led(RUBY2D_METHOD_ARGS_VARIADIC) {
1533
+ RUBY2D_EXTRACT_VARIADIC;
1534
+ if (argc != 5) r_raise("Ruby2D::Ext.window_gamepad_set_led expects 5 args, got %d", (int)argc);
1535
+ R_VAL id = argv[1];
1536
+ R_VAL r = argv[2];
1537
+ R_VAL g = argv[3];
1538
+ R_VAL b = argv[4];
1539
+ bool ok = R2D_SetGamepadLED(
1540
+ (SDL_JoystickID)NUM2INT(id),
1541
+ NUM2INT(r), NUM2INT(g), NUM2INT(b)
1542
+ );
1543
+ return ok ? R_TRUE : R_FALSE;
1544
+ }
1545
+
1546
+
1547
+ /*
1548
+ * Map an SDL_GamepadType to one of the high-level Ruby symbols Gamepad
1549
+ * exposes (`:xbox`, `:playstation`, `:nintendo`, `:generic`, `:unknown`).
1550
+ * Used both for the cached `pad.type` and the `real_type` debug field.
1551
+ */
1552
+ static R_VAL ruby2d_gamepad_type_to_sym(SDL_GamepadType t) {
1553
+ switch (t) {
1554
+ case SDL_GAMEPAD_TYPE_XBOX360:
1555
+ case SDL_GAMEPAD_TYPE_XBOXONE:
1556
+ return r_char_to_sym("xbox");
1557
+ case SDL_GAMEPAD_TYPE_PS3:
1558
+ case SDL_GAMEPAD_TYPE_PS4:
1559
+ case SDL_GAMEPAD_TYPE_PS5:
1560
+ return r_char_to_sym("playstation");
1561
+ case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
1562
+ case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
1563
+ case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
1564
+ case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
1565
+ case SDL_GAMEPAD_TYPE_GAMECUBE:
1566
+ return r_char_to_sym("nintendo");
1567
+ case SDL_GAMEPAD_TYPE_STANDARD:
1568
+ return r_char_to_sym("generic");
1569
+ default:
1570
+ return r_char_to_sym("unknown");
1571
+ }
1572
+ }
1573
+
1574
+
1575
+ /*
1576
+ * Ruby2D::Window#ext_gamepad_type
1577
+ */
1578
+ R_VAL ruby2d_ext_window_gamepad_type(RUBY2D_METHOD_ARGS_VARIADIC) {
1579
+ RUBY2D_EXTRACT_VARIADIC;
1580
+ if (argc != 2) r_raise("Ruby2D::Ext.window_gamepad_type expects 2 args, got %d", (int)argc);
1581
+ R_VAL id = argv[1];
1582
+ SDL_Gamepad *pad = SDL_GetGamepadFromID((SDL_JoystickID)NUM2INT(id));
1583
+ if (!pad) return r_char_to_sym("unknown");
1584
+ return ruby2d_gamepad_type_to_sym(SDL_GetGamepadType(pad));
1585
+ }
1586
+
1587
+
1588
+ /*
1589
+ * Ruby2D::Window#ext_gamepad_battery
1590
+ *
1591
+ * Returns a 2-element array: [state_symbol, percent]. State is one of
1592
+ * :wired / :on_battery / :unknown; percent is an Integer 0..100 or -1
1593
+ * if SDL did not report one.
1594
+ */
1595
+ R_VAL ruby2d_ext_window_gamepad_battery(RUBY2D_METHOD_ARGS_VARIADIC) {
1596
+ RUBY2D_EXTRACT_VARIADIC;
1597
+ if (argc != 2) r_raise("Ruby2D::Ext.window_gamepad_battery expects 2 args, got %d", (int)argc);
1598
+ R_VAL id = argv[1];
1599
+ int percent = -1;
1600
+ int state = R2D_GetGamepadBattery((SDL_JoystickID)NUM2INT(id), &percent);
1601
+ R_VAL state_sym;
1602
+ switch (state) {
1603
+ case R2D_GAMEPAD_BATTERY_WIRED: state_sym = r_char_to_sym("wired"); break;
1604
+ case R2D_GAMEPAD_BATTERY_ON_BATTERY: state_sym = r_char_to_sym("on_battery"); break;
1605
+ default: state_sym = r_char_to_sym("unknown"); break;
1606
+ }
1607
+ R_VAL ary = r_ary_new();
1608
+ r_ary_push(ary, state_sym);
1609
+ r_ary_push(ary, INT2NUM(percent));
1610
+ return ary;
1611
+ }
1612
+
1613
+
1614
+ /*
1615
+ * Ruby2D::Window#ext_gamepad_has_button
1616
+ */
1617
+ R_VAL ruby2d_ext_window_gamepad_has_button(RUBY2D_METHOD_ARGS_VARIADIC) {
1618
+ RUBY2D_EXTRACT_VARIADIC;
1619
+ if (argc != 3) r_raise("Ruby2D::Ext.window_gamepad_has_button expects 3 args, got %d", (int)argc);
1620
+ R_VAL id = argv[1];
1621
+ R_VAL button = argv[2];
1622
+ bool ok = R2D_GamepadHasButton(
1623
+ (SDL_JoystickID)NUM2INT(id), NUM2INT(button)
1624
+ );
1625
+ return ok ? R_TRUE : R_FALSE;
1626
+ }
1627
+
1628
+
1629
+ /*
1630
+ * Ruby2D::Window#ext_gamepad_has_axis
1631
+ */
1632
+ R_VAL ruby2d_ext_window_gamepad_has_axis(RUBY2D_METHOD_ARGS_VARIADIC) {
1633
+ RUBY2D_EXTRACT_VARIADIC;
1634
+ if (argc != 3) r_raise("Ruby2D::Ext.window_gamepad_has_axis expects 3 args, got %d", (int)argc);
1635
+ R_VAL id = argv[1];
1636
+ R_VAL axis = argv[2];
1637
+ bool ok = R2D_GamepadHasAxis(
1638
+ (SDL_JoystickID)NUM2INT(id), NUM2INT(axis)
1639
+ );
1640
+ return ok ? R_TRUE : R_FALSE;
1641
+ }
1642
+
1643
+
1644
+ /*
1645
+ * Ruby2D::Window#ext_gamepad_caps
1646
+ */
1647
+ R_VAL ruby2d_ext_window_gamepad_caps(RUBY2D_METHOD_ARGS_VARIADIC) {
1648
+ RUBY2D_EXTRACT_VARIADIC;
1649
+ if (argc != 2) r_raise("Ruby2D::Ext.window_gamepad_caps expects 2 args, got %d", (int)argc);
1650
+ R_VAL id = argv[1];
1651
+ int caps = R2D_GetGamepadCaps((SDL_JoystickID)NUM2INT(id));
1652
+ return INT2NUM(caps);
1653
+ }
1654
+
1655
+
1656
+ /*
1657
+ * Ruby2D::Window#ext_gamepad_debug_info
1658
+ *
1659
+ * Returns nil if the pad isn't open. Otherwise returns a fixed-shape
1660
+ * Array (positional) that `Gamepad#debug_info` reshapes into a Hash:
1661
+ * [guid_str, vendor, product, version, serial_or_nil,
1662
+ * connection_sym, real_type_sym, num_touchpads, mapping_or_nil]
1663
+ */
1664
+ R_VAL ruby2d_ext_window_gamepad_debug_info(RUBY2D_METHOD_ARGS_VARIADIC) {
1665
+ RUBY2D_EXTRACT_VARIADIC;
1666
+ if (argc != 2) r_raise("Ruby2D::Ext.window_gamepad_debug_info expects 2 args, got %d", (int)argc);
1667
+ R_VAL id = argv[1];
1668
+ SDL_JoystickID jid = (SDL_JoystickID)NUM2INT(id);
1669
+ SDL_Gamepad *pad = SDL_GetGamepadFromID(jid);
1670
+ if (!pad) return R_NIL;
1671
+
1672
+ R_VAL arr = r_ary_new();
1673
+
1674
+ // GUID — 32-char hex string
1675
+ SDL_GUID guid = SDL_GetGamepadGUIDForID(jid);
1676
+ char guid_str[33] = {0};
1677
+ SDL_GUIDToString(guid, guid_str, sizeof(guid_str));
1678
+ r_ary_push(arr, r_str_new(guid_str));
1679
+
1680
+ // Vendor / Product / Version (USB-style 16-bit IDs)
1681
+ r_ary_push(arr, INT2NUM(SDL_GetGamepadVendor(pad)));
1682
+ r_ary_push(arr, INT2NUM(SDL_GetGamepadProduct(pad)));
1683
+ r_ary_push(arr, INT2NUM(SDL_GetGamepadProductVersion(pad)));
1684
+
1685
+ // Serial number — frequently NULL
1686
+ const char *serial = SDL_GetGamepadSerial(pad);
1687
+ r_ary_push(arr, serial ? r_str_new(serial) : R_NIL);
1688
+
1689
+ // Connection state
1690
+ R_VAL conn_sym;
1691
+ switch (SDL_GetGamepadConnectionState(pad)) {
1692
+ case SDL_JOYSTICK_CONNECTION_WIRED: conn_sym = r_char_to_sym("wired"); break;
1693
+ case SDL_JOYSTICK_CONNECTION_WIRELESS: conn_sym = r_char_to_sym("wireless"); break;
1694
+ default: conn_sym = r_char_to_sym("unknown"); break;
1695
+ }
1696
+ r_ary_push(arr, conn_sym);
1697
+
1698
+ // Real (un-remapped) gamepad type — useful for spotting type overrides
1699
+ // applied by SDL_GameControllerDB or user mappings.
1700
+ r_ary_push(arr, ruby2d_gamepad_type_to_sym(SDL_GetRealGamepadType(pad)));
1701
+
1702
+ // Touchpad count (PS4/PS5 = 1; most others = 0)
1703
+ r_ary_push(arr, INT2NUM(SDL_GetNumGamepadTouchpads(pad)));
1704
+
1705
+ // Resolved mapping string. SDL_GetGamepadMapping returns malloc'd memory
1706
+ // that the caller must release via SDL_free.
1707
+ char *mapping = SDL_GetGamepadMapping(pad);
1708
+ if (mapping) {
1709
+ r_ary_push(arr, r_str_new(mapping));
1710
+ SDL_free(mapping);
1711
+ } else {
1712
+ r_ary_push(arr, R_NIL);
1713
+ }
1714
+
1715
+ return arr;
1716
+ }
1717
+
1718
+
1719
+ /*
1720
+ * Ruby2D::Window#ext_gamepad_joystick_state
1721
+ */
1722
+ R_VAL ruby2d_ext_window_gamepad_joystick_state(RUBY2D_METHOD_ARGS_VARIADIC) {
1723
+ RUBY2D_EXTRACT_VARIADIC;
1724
+ if (argc != 2) r_raise("Ruby2D::Ext.window_gamepad_joystick_state expects 2 args, got %d", (int)argc);
1725
+ R_VAL id = argv[1];
1726
+ SDL_Gamepad *pad = SDL_GetGamepadFromID((SDL_JoystickID)NUM2INT(id));
1727
+ if (!pad) return R_NIL;
1728
+ SDL_Joystick *joy = SDL_GetGamepadJoystick(pad);
1729
+ if (!joy) return R_NIL;
1730
+
1731
+ R_VAL btns = r_ary_new();
1732
+ int n_btns = SDL_GetNumJoystickButtons(joy);
1733
+ for (int i = 0; i < n_btns; i++) {
1734
+ r_ary_push(btns, SDL_GetJoystickButton(joy, i) ? R_TRUE : R_FALSE);
1735
+ }
1736
+
1737
+ R_VAL axes = r_ary_new();
1738
+ int n_axes = SDL_GetNumJoystickAxes(joy);
1739
+ for (int i = 0; i < n_axes; i++) {
1740
+ r_ary_push(axes, INT2NUM(SDL_GetJoystickAxis(joy, i)));
1741
+ }
1742
+
1743
+ R_VAL hats = r_ary_new();
1744
+ int n_hats = SDL_GetNumJoystickHats(joy);
1745
+ for (int i = 0; i < n_hats; i++) {
1746
+ r_ary_push(hats, INT2NUM((int)SDL_GetJoystickHat(joy, i)));
1747
+ }
1748
+
1749
+ R_VAL result = r_ary_new();
1750
+ r_ary_push(result, btns);
1751
+ r_ary_push(result, axes);
1752
+ r_ary_push(result, hats);
1753
+ return result;
1754
+ }
1755
+
1756
+
1757
+ /*
1758
+ * Ruby2D::Window#ext_screenshot
1759
+ *
1760
+ * Defers the screenshot to the end of the current frame, after rendering but
1761
+ * before SDL_RenderPresent, so the back buffer contains the fully drawn scene.
1762
+ */
1763
+ R_VAL ruby2d_ext_window_screenshot(RUBY2D_METHOD_ARGS_VARIADIC) {
1764
+ RUBY2D_EXTRACT_VARIADIC;
1765
+ if (argc != 2) r_raise("Ruby2D::Ext.window_screenshot expects 2 args, got %d", (int)argc);
1766
+ R_VAL path = argv[1];
1767
+ if (r2d_window) {
1768
+ free(r2d_window->screenshot_path);
1769
+ r2d_window->screenshot_path = strdup(RSTRING_PTR(path));
1770
+ return path;
1771
+ } else {
1772
+ return R_FALSE;
1773
+ }
1774
+ }
1775
+
1776
+
1777
+ /*
1778
+ * Ruby2D::Window#ext_show_cursor
1779
+ */
1780
+ R_VAL ruby2d_ext_window_show_cursor(RUBY2D_METHOD_ARGS_VARIADIC) {
1781
+ RUBY2D_EXTRACT_VARIADIC;
1782
+ if (argc != 1) r_raise("Ruby2D::Ext.window_show_cursor expects 1 arg (window), got %d", (int)argc);
1783
+ R2D_ShowCursor();
1784
+ return R_TRUE;
1785
+ }
1786
+
1787
+
1788
+ /*
1789
+ * Ruby2D::Window#ext_hide_cursor
1790
+ */
1791
+ R_VAL ruby2d_ext_window_hide_cursor(RUBY2D_METHOD_ARGS_VARIADIC) {
1792
+ RUBY2D_EXTRACT_VARIADIC;
1793
+ if (argc != 1) r_raise("Ruby2D::Ext.window_hide_cursor expects 1 arg (window), got %d", (int)argc);
1794
+ R2D_HideCursor();
1795
+ return R_TRUE;
1796
+ }
1797
+
1798
+
1799
+ /*
1800
+ * Ruby2D::Window#ext_cursor_visible
1801
+ */
1802
+ R_VAL ruby2d_ext_window_cursor_visible(RUBY2D_METHOD_ARGS_VARIADIC) {
1803
+ RUBY2D_EXTRACT_VARIADIC;
1804
+ if (argc != 1) r_raise("Ruby2D::Ext.window_cursor_visible expects 1 arg (window), got %d", (int)argc);
1805
+ return R2D_CursorVisible() ? R_TRUE : R_FALSE;
1806
+ }
1807
+
1808
+
1809
+ /*
1810
+ * Ruby2D::Window#ext_set_system_cursor
1811
+ */
1812
+ R_VAL ruby2d_ext_window_set_system_cursor(RUBY2D_METHOD_ARGS_VARIADIC) {
1813
+ RUBY2D_EXTRACT_VARIADIC;
1814
+ if (argc != 2) r_raise("Ruby2D::Ext.window_set_system_cursor expects 2 args, got %d", (int)argc);
1815
+ R_VAL name = argv[1];
1816
+ return R2D_SetSystemCursor(RSTRING_PTR(name)) ? R_TRUE : R_FALSE;
1817
+ }
1818
+
1819
+
1820
+ /*
1821
+ * Ruby2D::Window#ext_close
1822
+ */
1823
+ R_VAL ruby2d_ext_window_close(RUBY2D_METHOD_ARGS_VARIADIC) {
1824
+ RUBY2D_EXTRACT_VARIADIC;
1825
+ if (argc != 1) r_raise("Ruby2D::Ext.window_close expects 1 arg (window), got %d", (int)argc);
1826
+ R2D_Close(r2d_window);
1827
+ return R_NIL;
1828
+ }
1829
+
1830
+
1831
+ /*
1832
+ * Ruby2D::Window#ext_set_render_mode
1833
+ *
1834
+ * Reads @render_mode from Ruby and updates the native render mode. Marks the
1835
+ * next frame pending so the app always sees at least one frame after switching
1836
+ * modes (particularly important when switching from on_demand → continuous or
1837
+ * changing modes mid-scene).
1838
+ */
1839
+ R_VAL ruby2d_ext_window_set_render_mode(RUBY2D_METHOD_ARGS_VARIADIC) {
1840
+ RUBY2D_EXTRACT_VARIADIC;
1841
+ if (argc != 1) r_raise("Ruby2D::Ext.window_set_render_mode expects 1 arg (window), got %d", (int)argc);
1842
+ R_VAL obj = argv[0];
1843
+ if (!r2d_window) return R_FALSE;
1844
+ r2d_window->render_mode = R2D_ParseRenderMode(r_ivar_get(obj, id_render_mode));
1845
+ SDL_SetAtomicInt(&r2d_window->render_pending, 1);
1846
+ return R_TRUE;
1847
+ }
1848
+
1849
+
1850
+ /*
1851
+ * Ruby2D::Window#ext_request_render
1852
+ *
1853
+ * Marks the next tick as needing a rendered frame. Safe to call from any
1854
+ * thread (the dirty flag is an SDL atomic int).
1855
+ */
1856
+ R_VAL ruby2d_ext_window_request_render(RUBY2D_METHOD_ARGS_VARIADIC) {
1857
+ RUBY2D_EXTRACT_VARIADIC;
1858
+ if (argc != 1) r_raise("Ruby2D::Ext.window_request_render expects 1 arg (window), got %d", (int)argc);
1859
+ if (!r2d_window) return R_FALSE;
1860
+ SDL_SetAtomicInt(&r2d_window->render_pending, 1);
1861
+ return R_TRUE;
1862
+ }