rgame 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +406 -0
  4. data/docs/api/README.md +167 -0
  5. data/docs/api/app.md +192 -0
  6. data/docs/api/assets.md +426 -0
  7. data/docs/api/audio.md +208 -0
  8. data/docs/api/components.md +321 -0
  9. data/docs/api/drawing.md +330 -0
  10. data/docs/api/game.md +99 -0
  11. data/docs/api/images.md +118 -0
  12. data/docs/api/input.md +179 -0
  13. data/docs/api/internals.md +110 -0
  14. data/docs/api/scene_graph.md +159 -0
  15. data/docs/api/signals.md +142 -0
  16. data/docs/api/systems.md +98 -0
  17. data/docs/api/text.md +116 -0
  18. data/docs/api/toolbox.md +240 -0
  19. data/docs/api/values.md +101 -0
  20. data/ext/README.md +225 -0
  21. data/ext/rgame_core/app/app.c +721 -0
  22. data/ext/rgame_core/app/app_gl.h +64 -0
  23. data/ext/rgame_core/app/frame_loop.c +42 -0
  24. data/ext/rgame_core/app/frame_loop.h +54 -0
  25. data/ext/rgame_core/audio/audio.c +466 -0
  26. data/ext/rgame_core/audio/audio_internal.h +45 -0
  27. data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
  28. data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
  29. data/ext/rgame_core/example.rb +188 -0
  30. data/ext/rgame_core/extconf.rb +167 -0
  31. data/ext/rgame_core/graphics/backend.c +52 -0
  32. data/ext/rgame_core/graphics/backend.h +64 -0
  33. data/ext/rgame_core/graphics/canvas.c +247 -0
  34. data/ext/rgame_core/graphics/canvas.h +143 -0
  35. data/ext/rgame_core/graphics/clip.c +87 -0
  36. data/ext/rgame_core/graphics/clip.h +89 -0
  37. data/ext/rgame_core/graphics/draw_queue.c +216 -0
  38. data/ext/rgame_core/graphics/draw_queue.h +174 -0
  39. data/ext/rgame_core/graphics/gl_backend.c +122 -0
  40. data/ext/rgame_core/graphics/gl_backend.h +43 -0
  41. data/ext/rgame_core/graphics/image.c +304 -0
  42. data/ext/rgame_core/graphics/image_internal.h +30 -0
  43. data/ext/rgame_core/graphics/primitives.c +189 -0
  44. data/ext/rgame_core/graphics/primitives.h +111 -0
  45. data/ext/rgame_core/graphics/recording.c +119 -0
  46. data/ext/rgame_core/graphics/recording.h +88 -0
  47. data/ext/rgame_core/graphics/texture.c +181 -0
  48. data/ext/rgame_core/graphics/texture.h +165 -0
  49. data/ext/rgame_core/graphics/transform.c +128 -0
  50. data/ext/rgame_core/graphics/transform.h +106 -0
  51. data/ext/rgame_core/include/rgame/core.h +577 -0
  52. data/ext/rgame_core/input/device_slots.c +103 -0
  53. data/ext/rgame_core/input/device_slots.h +93 -0
  54. data/ext/rgame_core/input/gamepad.c +145 -0
  55. data/ext/rgame_core/input/gamepad.h +63 -0
  56. data/ext/rgame_core/input/input.c +109 -0
  57. data/ext/rgame_core/input/input.h +99 -0
  58. data/ext/rgame_core/ruby/audio_ext.c +321 -0
  59. data/ext/rgame_core/ruby/core_ext.c +513 -0
  60. data/ext/rgame_core/ruby/core_ext.h +51 -0
  61. data/ext/rgame_core/ruby/font_ext.c +168 -0
  62. data/ext/rgame_core/ruby/image_ext.c +230 -0
  63. data/ext/rgame_core/ruby/recording_ext.c +186 -0
  64. data/ext/rgame_core/ruby/renderer_ext.c +376 -0
  65. data/ext/rgame_core/text/atlas.c +59 -0
  66. data/ext/rgame_core/text/atlas.h +85 -0
  67. data/ext/rgame_core/text/font.c +281 -0
  68. data/ext/rgame_core/text/font.h +139 -0
  69. data/ext/rgame_core/text/font_atlas.c +385 -0
  70. data/ext/rgame_core/text/font_internal.h +47 -0
  71. data/ext/rgame_core/text/glyph_cache.c +142 -0
  72. data/ext/rgame_core/text/glyph_cache.h +89 -0
  73. data/ext/rgame_core/vendor/README.md +159 -0
  74. data/ext/rgame_core/vendor/miniaudio.h +95864 -0
  75. data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
  76. data/ext/rgame_core/vendor/stb_image.h +7988 -0
  77. data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
  78. data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
  79. data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
  80. data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
  81. data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
  82. data/ext/rgame_util/color.c +19 -0
  83. data/ext/rgame_util/color.h +60 -0
  84. data/ext/rgame_util/color_ext.c +156 -0
  85. data/ext/rgame_util/extconf.rb +27 -0
  86. data/ext/rgame_util/tensor.c +186 -0
  87. data/ext/rgame_util/util_ext.c +27 -0
  88. data/ext/rgame_util/util_ext.h +16 -0
  89. data/lib/rgame/boot.rb +13 -0
  90. data/lib/rgame/core/app.rb +82 -0
  91. data/lib/rgame/core/asset_manager.rb +224 -0
  92. data/lib/rgame/core/audio.rb +124 -0
  93. data/lib/rgame/core/font.rb +49 -0
  94. data/lib/rgame/core/gamepad.rb +55 -0
  95. data/lib/rgame/core/image.rb +55 -0
  96. data/lib/rgame/core/input.rb +77 -0
  97. data/lib/rgame/core/nine_slice.rb +163 -0
  98. data/lib/rgame/core/recording.rb +52 -0
  99. data/lib/rgame/core/renderer.rb +363 -0
  100. data/lib/rgame/core/sprite_sheet.rb +108 -0
  101. data/lib/rgame/core/tile_map_renderer.rb +160 -0
  102. data/lib/rgame/core/ui_atlas.rb +86 -0
  103. data/lib/rgame/core.rb +24 -0
  104. data/lib/rgame/engine/actor.rb +53 -0
  105. data/lib/rgame/engine/animation_set.rb +49 -0
  106. data/lib/rgame/engine/animator.rb +44 -0
  107. data/lib/rgame/engine/audio_bus.rb +24 -0
  108. data/lib/rgame/engine/audio_director.rb +29 -0
  109. data/lib/rgame/engine/body.rb +49 -0
  110. data/lib/rgame/engine/cached_label.rb +33 -0
  111. data/lib/rgame/engine/camera.rb +33 -0
  112. data/lib/rgame/engine/camera_view.rb +28 -0
  113. data/lib/rgame/engine/circle_collider.rb +32 -0
  114. data/lib/rgame/engine/collision_box.rb +34 -0
  115. data/lib/rgame/engine/collision_system.rb +44 -0
  116. data/lib/rgame/engine/component.rb +30 -0
  117. data/lib/rgame/engine/components/action_trigger.rb +41 -0
  118. data/lib/rgame/engine/components/animated_sprite.rb +63 -0
  119. data/lib/rgame/engine/components/character_body.rb +70 -0
  120. data/lib/rgame/engine/components/circle_collider.rb +44 -0
  121. data/lib/rgame/engine/components/collision_world.rb +103 -0
  122. data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
  123. data/lib/rgame/engine/components/path_follow.rb +84 -0
  124. data/lib/rgame/engine/components/player_controller.rb +24 -0
  125. data/lib/rgame/engine/components/pool.rb +53 -0
  126. data/lib/rgame/engine/components/screen_wrap.rb +27 -0
  127. data/lib/rgame/engine/components/sprite.rb +31 -0
  128. data/lib/rgame/engine/components/targeting.rb +54 -0
  129. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  130. data/lib/rgame/engine/components/tile_world.rb +68 -0
  131. data/lib/rgame/engine/components/timer.rb +75 -0
  132. data/lib/rgame/engine/components/velocity.rb +27 -0
  133. data/lib/rgame/engine/components/wander_controller.rb +60 -0
  134. data/lib/rgame/engine/debug_overlay.rb +106 -0
  135. data/lib/rgame/engine/i18n.rb +97 -0
  136. data/lib/rgame/engine/input/action_mapper.rb +46 -0
  137. data/lib/rgame/engine/input/actions.rb +41 -0
  138. data/lib/rgame/engine/input/player_controller.rb +14 -0
  139. data/lib/rgame/engine/matrix.rb +32 -0
  140. data/lib/rgame/engine/node2d.rb +271 -0
  141. data/lib/rgame/engine/path.rb +78 -0
  142. data/lib/rgame/engine/pool.rb +51 -0
  143. data/lib/rgame/engine/resettable.rb +67 -0
  144. data/lib/rgame/engine/scene/scene_stack.rb +65 -0
  145. data/lib/rgame/engine/signal.rb +75 -0
  146. data/lib/rgame/engine/spatial_hash.rb +71 -0
  147. data/lib/rgame/engine/tile_collision.rb +78 -0
  148. data/lib/rgame/engine/tile_map.rb +149 -0
  149. data/lib/rgame/engine/tileset.rb +101 -0
  150. data/lib/rgame/engine/timer.rb +51 -0
  151. data/lib/rgame/engine.rb +68 -0
  152. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  153. data/lib/rgame/fonts/OFL.txt +102 -0
  154. data/lib/rgame/game.rb +129 -0
  155. data/lib/rgame/util/color.rb +27 -0
  156. data/lib/rgame/util/controls.rb +107 -0
  157. data/lib/rgame/util/tensor.rb +12 -0
  158. data/lib/rgame/util.rb +8 -0
  159. data/lib/rgame/version.rb +12 -0
  160. data/lib/rgame.rb +20 -0
  161. metadata +215 -0
@@ -0,0 +1,168 @@
1
+ /*
2
+ * font_ext.c — the Ruby binding for RGame::Core::Font.
3
+ *
4
+ * font = RGame::Core::Font.new(app, 18) # the shipped font
5
+ * font = RGame::Core::Font.new(app, 18, path: 'assets/x.ttf')
6
+ * font.height # => 18
7
+ * font.text_width('Score: 1200')
8
+ *
9
+ * The atlas, the cache and the metrics are in font_atlas.c, font.c, atlas.c and
10
+ * glyph_cache.c — three of those four are pure and Check-tested. This file is
11
+ * the wrapper: argument checking, turning a NULL return into an exception that
12
+ * says why, and keeping the app reachable for as long as the font is.
13
+ *
14
+ * The default path is *not* here. It is a packaging question — where a gem
15
+ * installs its data — so lib/rgame/core/font.rb owns it and passes a path down.
16
+ *
17
+ * The C layer has no opinion about where fonts live and there is **no
18
+ * font-name lookup at all**: a font is a file. Asking the operating system for
19
+ * one by name would mean a font-database backend per platform — fontconfig on
20
+ * Linux, CoreText on macOS, GDI on Windows — and a system dependency this
21
+ * engine otherwise does not have. It would also mean text renders differently
22
+ * on different machines, which is a layout bug nobody can reproduce. See
23
+ * vendor/README.md.
24
+ */
25
+
26
+ #include "ruby/core_ext.h"
27
+
28
+ #include "text/font_internal.h"
29
+ #include "rgame/core.h"
30
+
31
+ typedef struct {
32
+ rgame_font *font;
33
+ VALUE app_object; /* marked: the atlas pages live in this window's context */
34
+ } rgame_font_ref;
35
+
36
+ static void font_ref_mark(void *ptr) {
37
+ rgame_font_ref *ref = ptr;
38
+ rb_gc_mark(ref->app_object);
39
+ }
40
+
41
+ static void font_ref_free(void *ptr) {
42
+ rgame_font_ref *ref = ptr;
43
+ rgame_font_destroy(ref->font); /* NULL-safe */
44
+ xfree(ref);
45
+ }
46
+
47
+ static size_t font_ref_size(const void *ptr) {
48
+ (void)ptr;
49
+ return sizeof(rgame_font_ref);
50
+ }
51
+
52
+ static const rb_data_type_t font_data_type = {
53
+ .wrap_struct_name = "rgame_font",
54
+ .function = {
55
+ .dmark = font_ref_mark,
56
+ .dfree = font_ref_free,
57
+ .dsize = font_ref_size,
58
+ },
59
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
60
+ };
61
+
62
+ static VALUE cFont;
63
+
64
+ static rgame_font_ref *font_unwrap(VALUE self) {
65
+ rgame_font_ref *ref;
66
+ TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
67
+ if (!ref->font) {
68
+ rb_raise(rb_eRuntimeError, "font is not initialized");
69
+ }
70
+ return ref;
71
+ }
72
+
73
+ rgame_font *rgame_font_unwrap(VALUE font) {
74
+ return font_unwrap(font)->font;
75
+ }
76
+
77
+ static VALUE font_alloc(VALUE klass) {
78
+ rgame_font_ref *ref;
79
+ VALUE object = TypedData_Make_Struct(klass, rgame_font_ref, &font_data_type, ref);
80
+ ref->font = NULL;
81
+ ref->app_object = Qnil;
82
+ return object;
83
+ }
84
+
85
+ /* Font.new(app, pixel_height, path) — the path is supplied by the Ruby half,
86
+ * which defaults it to the shipped font. */
87
+ static VALUE font_initialize(VALUE self, VALUE app, VALUE pixel_height, VALUE path) {
88
+ rgame_font_ref *ref;
89
+ TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
90
+
91
+ rgame_app *engine_app = rgame_app_unwrap(app); /* raises TypeError otherwise */
92
+ const char *path_str = StringValueCStr(path);
93
+ int height = NUM2INT(pixel_height);
94
+
95
+ char error[256] = {0};
96
+ rgame_font *font = rgame_font_load(engine_app, path_str, height, error, sizeof(error));
97
+ if (!font) {
98
+ rb_raise(rb_const_get(cFont, rb_intern("LoadError")), "%s", error);
99
+ }
100
+
101
+ ref->font = font;
102
+ ref->app_object = app;
103
+ return self;
104
+ }
105
+
106
+ /* The size it was loaded at, and the amount to step by for a second line. */
107
+ static VALUE font_height(VALUE self) {
108
+ return INT2NUM(rgame_font_height(font_unwrap(self)->font));
109
+ }
110
+
111
+ /*
112
+ * #text_width(string) — what #text would occupy, in pixels.
113
+ *
114
+ * Deliberately usable outside `draw`: measuring touches no GL, and laying out a
115
+ * menu happens while updating, not while drawing.
116
+ */
117
+ static VALUE font_text_width(VALUE self, VALUE string) {
118
+ rgame_font_ref *ref = font_unwrap(self);
119
+
120
+ /* Raises TypeError rather than letting RSTRING_PTR read a non-String's
121
+ * innards as a char*, which segfaults. See renderer_ext.c's #draw_text. */
122
+ StringValue(string);
123
+
124
+ /* The bytes as Ruby holds them. No copy, no per-call allocation, and the
125
+ * codepoint walk happens in C where it costs nothing. */
126
+ const char *text = RSTRING_PTR(string);
127
+ long length = RSTRING_LEN(string);
128
+ double width = rgame_font_measure(ref->font, text, (size_t)length);
129
+
130
+ /* Keeps `string` alive across the call above, which matters because
131
+ * RSTRING_PTR hands out a pointer the GC does not know we are holding. */
132
+ RB_GC_GUARD(string);
133
+ return DBL2NUM(width);
134
+ }
135
+
136
+ /* How many atlas pages exist across every live font. Test-only, and named so:
137
+ * a leaked page is invisible — nothing looks wrong until video memory runs
138
+ * out. The image side has the same counter for the same reason. */
139
+ static VALUE font_s_debug_live_pages(VALUE klass) {
140
+ (void)klass;
141
+ return LONG2NUM(rgame_font_live_pages());
142
+ }
143
+
144
+ static VALUE font_inspect(VALUE self) {
145
+ rgame_font_ref *ref;
146
+ TypedData_Get_Struct(self, rgame_font_ref, &font_data_type, ref);
147
+ if (!ref->font) {
148
+ return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
149
+ }
150
+
151
+ return rb_sprintf("#<%" PRIsVALUE " %dpx>", rb_obj_class(self),
152
+ rgame_font_height(ref->font));
153
+ }
154
+
155
+ void rgame_init_font(VALUE mCore) {
156
+ cFont = rb_define_class_under(mCore, "Font", rb_cObject);
157
+
158
+ /* Raised when a file cannot be read or is not a TrueType font — an everyday
159
+ * condition, so it gets a name a game can rescue. */
160
+ rb_define_class_under(cFont, "LoadError", rb_eStandardError);
161
+
162
+ rb_define_alloc_func(cFont, font_alloc);
163
+ rb_define_method(cFont, "initialize", font_initialize, 3);
164
+ rb_define_method(cFont, "height", font_height, 0);
165
+ rb_define_method(cFont, "text_width", font_text_width, 1);
166
+ rb_define_method(cFont, "inspect", font_inspect, 0);
167
+ rb_define_singleton_method(cFont, "debug_live_pages", font_s_debug_live_pages, 0);
168
+ }
@@ -0,0 +1,230 @@
1
+ /*
2
+ * image_ext.c — the Ruby binding for RGame::Core::Image.
3
+ *
4
+ * The decode and upload are in image.c; the rectangle arithmetic behind
5
+ * subimages and tiles is in texture.c and is Check-tested with no GPU. This
6
+ * file is only the wrapper: argument checking, turning C's "returned NULL"
7
+ * into a Ruby exception that says why, and keeping the app object reachable
8
+ * for as long as any image made from it is.
9
+ *
10
+ * img = RGame::Core::Image.new(app, 'hero.png')
11
+ * img.width; img.height
12
+ * sub = img.subimage(0, 0, 16, 16)
13
+ * tiles = RGame::Core::Image.load_tiles(app, 'sheet.png', 16, 16)
14
+ *
15
+ * Slicing never re-decodes: every image above shares one GPU texture, and the
16
+ * texture goes when the last of them does.
17
+ */
18
+
19
+ #include "ruby/core_ext.h"
20
+
21
+ #include "rgame/core.h"
22
+ #include "graphics/texture.h"
23
+
24
+ /*
25
+ * The Ruby object's payload. Two members, and the second is the interesting
26
+ * one: `app` is the Ruby App object this image was loaded from, marked so the
27
+ * collector treats the app as reachable from every image.
28
+ *
29
+ * That is a deliberate ownership statement, not just GC hygiene. An image is
30
+ * meaningless without the context its pixels live in, and a game that keeps a
31
+ * sprite around has, by definition, not finished with the window. (The C layer
32
+ * survives the other order too — see rgame_app_gl_retain — but a Ruby user
33
+ * should never be in a position to observe it.)
34
+ */
35
+ typedef struct {
36
+ rgame_image *image;
37
+ VALUE app;
38
+ } rgame_image_ref;
39
+
40
+ static void image_ref_mark(void *ptr) {
41
+ rgame_image_ref *ref = ptr;
42
+ rb_gc_mark(ref->app);
43
+ }
44
+
45
+ static void image_ref_free(void *ptr) {
46
+ rgame_image_ref *ref = ptr;
47
+ rgame_image_destroy(ref->image); /* NULL-safe */
48
+ xfree(ref);
49
+ }
50
+
51
+ static size_t image_ref_size(const void *ptr) {
52
+ (void)ptr;
53
+ return sizeof(rgame_image_ref);
54
+ }
55
+
56
+ static const rb_data_type_t image_data_type = {
57
+ .wrap_struct_name = "rgame_image",
58
+ .function = {
59
+ .dmark = image_ref_mark,
60
+ .dfree = image_ref_free,
61
+ .dsize = image_ref_size,
62
+ },
63
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
64
+ };
65
+
66
+ static VALUE cImage;
67
+
68
+ static rgame_image_ref *image_ref_unwrap(VALUE self) {
69
+ rgame_image_ref *ref;
70
+ TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
71
+ if (!ref->image) {
72
+ rb_raise(rb_eRuntimeError, "image is not initialized");
73
+ }
74
+ return ref;
75
+ }
76
+
77
+ /* The accessor other files in this extension use (see core_ext.h) — the
78
+ * renderer needs the C handle to draw. TypedData_Get_Struct raises TypeError on
79
+ * anything that is not an Image, so the type check comes for free. */
80
+ rgame_image *rgame_image_unwrap(VALUE image) {
81
+ return image_ref_unwrap(image)->image;
82
+ }
83
+
84
+ /*
85
+ * Wraps a C handle in a fresh Ruby object. Takes ownership: if allocating the
86
+ * Ruby side raises, the handle would leak, so the payload is filled in before
87
+ * anything else can fail.
88
+ */
89
+ static VALUE image_wrap(rgame_image *image, VALUE app) {
90
+ rgame_image_ref *ref;
91
+ VALUE object = TypedData_Make_Struct(cImage, rgame_image_ref, &image_data_type, ref);
92
+ ref->image = image;
93
+ ref->app = app;
94
+ return object;
95
+ }
96
+
97
+ /*
98
+ * Image.new(app, path)
99
+ *
100
+ * `app` comes first and is required, rather than being implied by "the window
101
+ * that happens to be open". Textures belong to one GL context, so an image
102
+ * genuinely is an image *of* an app — and saying so is what lets the object
103
+ * hold the app reachable, and what makes two windows work at all.
104
+ */
105
+ static VALUE image_initialize(VALUE self, VALUE app, VALUE path) {
106
+ rgame_image_ref *ref;
107
+ TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
108
+
109
+ /* Unwrapping raises TypeError on anything that is not an App, so there is
110
+ * no separate type check to keep in step with this one. */
111
+ rgame_app *engine_app = rgame_app_unwrap(app);
112
+ const char *path_str = StringValueCStr(path);
113
+
114
+ char error[256] = {0};
115
+ rgame_image *image = rgame_image_load(engine_app, path_str, error, sizeof(error));
116
+ if (!image) {
117
+ rb_raise(rb_const_get(cImage, rb_intern("LoadError")), "%s", error);
118
+ }
119
+
120
+ ref->image = image;
121
+ ref->app = app;
122
+ return self;
123
+ }
124
+
125
+ static VALUE image_alloc(VALUE klass) {
126
+ rgame_image_ref *ref;
127
+ VALUE object = TypedData_Make_Struct(klass, rgame_image_ref, &image_data_type, ref);
128
+ ref->image = NULL;
129
+ ref->app = Qnil;
130
+ return object;
131
+ }
132
+
133
+ static VALUE image_width(VALUE self) {
134
+ return INT2NUM(rgame_image_width(image_ref_unwrap(self)->image));
135
+ }
136
+
137
+ static VALUE image_height(VALUE self) {
138
+ return INT2NUM(rgame_image_height(image_ref_unwrap(self)->image));
139
+ }
140
+
141
+ /*
142
+ * #subimage(x, y, width, height)
143
+ *
144
+ * Raises rather than returning nil on a rect that does not fit. A nil here
145
+ * would travel a long way — into an asset table, out of it three scenes
146
+ * later — before failing as a NoMethodError with nothing left pointing at the
147
+ * bad coordinates.
148
+ */
149
+ static VALUE image_subimage(VALUE self, VALUE x, VALUE y, VALUE width, VALUE height) {
150
+ rgame_image_ref *ref = image_ref_unwrap(self);
151
+
152
+ rgame_image *sub = rgame_image_subimage(ref->image, NUM2INT(x), NUM2INT(y), NUM2INT(width),
153
+ NUM2INT(height));
154
+ if (!sub) {
155
+ rb_raise(rb_eArgError,
156
+ "subimage %" PRIsVALUE ",%" PRIsVALUE " %" PRIsVALUE "x%" PRIsVALUE
157
+ " does not fit in a %dx%d image",
158
+ x, y, width, height, rgame_image_width(ref->image),
159
+ rgame_image_height(ref->image));
160
+ }
161
+
162
+ return image_wrap(sub, ref->app);
163
+ }
164
+
165
+ /* #tile_count(tile_width, tile_height) — whole tiles only. */
166
+ static VALUE image_tile_count(VALUE self, VALUE tile_width, VALUE tile_height) {
167
+ return INT2NUM(rgame_image_tile_count(image_ref_unwrap(self)->image, NUM2INT(tile_width),
168
+ NUM2INT(tile_height)));
169
+ }
170
+
171
+ /* #tile(tile_width, tile_height, index) — row-major, left to right then down. */
172
+ static VALUE image_tile(VALUE self, VALUE tile_width, VALUE tile_height, VALUE index) {
173
+ rgame_image_ref *ref = image_ref_unwrap(self);
174
+
175
+ rgame_image *tile = rgame_image_tile(ref->image, NUM2INT(tile_width), NUM2INT(tile_height),
176
+ NUM2INT(index));
177
+ if (!tile) {
178
+ rb_raise(rb_eIndexError, "tile %" PRIsVALUE " is out of range (%d tiles of %" PRIsVALUE
179
+ "x%" PRIsVALUE ")",
180
+ index,
181
+ rgame_image_tile_count(ref->image, NUM2INT(tile_width), NUM2INT(tile_height)),
182
+ tile_width, tile_height);
183
+ }
184
+
185
+ return image_wrap(tile, ref->app);
186
+ }
187
+
188
+ static VALUE image_inspect(VALUE self) {
189
+ rgame_image_ref *ref;
190
+ TypedData_Get_Struct(self, rgame_image_ref, &image_data_type, ref);
191
+ if (!ref->image) {
192
+ return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
193
+ }
194
+
195
+ return rb_sprintf("#<%" PRIsVALUE " %dx%d>", rb_obj_class(self),
196
+ rgame_image_width(ref->image), rgame_image_height(ref->image));
197
+ }
198
+
199
+ /*
200
+ * Image.debug_live_textures — how many GPU textures the engine is holding.
201
+ *
202
+ * Test-only, and named so. A leaked texture is silent: nothing is slower and
203
+ * nothing looks wrong until video memory runs out an hour into play. This is
204
+ * what lets a spec assert that dropping a sheet and all its tiles actually
205
+ * releases the upload — including in the case that only a garbage collector
206
+ * can produce, where an app and its images are swept in an arbitrary order.
207
+ */
208
+ static VALUE image_s_debug_live_textures(VALUE klass) {
209
+ (void)klass;
210
+ return LONG2NUM(rgame_texture_live_sheets());
211
+ }
212
+
213
+ void rgame_init_image(VALUE mCore) {
214
+ cImage = rb_define_class_under(mCore, "Image", rb_cObject);
215
+
216
+ /* Raised when a file cannot be read or decoded — an everyday condition
217
+ * (a typo'd path, a truncated asset), so it gets a name a game can rescue
218
+ * rather than a bare RuntimeError. */
219
+ rb_define_class_under(cImage, "LoadError", rb_eStandardError);
220
+
221
+ rb_define_alloc_func(cImage, image_alloc);
222
+ rb_define_method(cImage, "initialize", image_initialize, 2);
223
+ rb_define_method(cImage, "width", image_width, 0);
224
+ rb_define_method(cImage, "height", image_height, 0);
225
+ rb_define_method(cImage, "subimage", image_subimage, 4);
226
+ rb_define_method(cImage, "tile_count", image_tile_count, 2);
227
+ rb_define_method(cImage, "tile", image_tile, 3);
228
+ rb_define_method(cImage, "inspect", image_inspect, 0);
229
+ rb_define_singleton_method(cImage, "debug_live_textures", image_s_debug_live_textures, 0);
230
+ }
@@ -0,0 +1,186 @@
1
+ /*
2
+ * recording_ext.c — the Ruby binding for RGame::Core::Recording.
3
+ *
4
+ * A recording is made by `RGame::Core::Renderer#record`, never by `new`:
5
+ *
6
+ * ground = renderer.record { tiles.each { |t| renderer.image(t.image, t.x, t.y) } }
7
+ * ground.draw(-camera.x, -camera.y)
8
+ *
9
+ * The baking and the replay are in recording.c and canvas.c, both pure. What
10
+ * this file adds is the two things only Ruby can do: hold the images the
11
+ * recording drew, so their GPU textures cannot be freed while a recording still
12
+ * references them, and refuse a replay outside a frame.
13
+ *
14
+ * ---------------------------------------------------------------------------
15
+ * Why the images have to be held
16
+ * ---------------------------------------------------------------------------
17
+ *
18
+ * A baked batch stores a GL texture *name* — a number. If the Image it came
19
+ * from is collected, its texture is deleted and that number now refers to
20
+ * nothing; replaying would draw whatever the driver put there next, or nothing
21
+ * at all, with no error. So the renderer collects the images drawn during a
22
+ * recording, and the recording marks them for as long as it lives. Holding a
23
+ * texture alive while something still draws it is the same rule as everywhere
24
+ * else here; it just has to be spelled out at this layer, because the C side
25
+ * only ever saw a number.
26
+ */
27
+
28
+ #include "ruby/core_ext.h"
29
+
30
+ #include "graphics/recording.h"
31
+ #include "rgame/core.h"
32
+
33
+ typedef struct {
34
+ rgame_recording *recording;
35
+ rgame_app *app;
36
+ VALUE app_object; /* marked: the textures live in this window's context */
37
+ VALUE images; /* marked: an Array of every Image baked into this */
38
+ } rgame_recording_ref;
39
+
40
+ static void recording_mark(void *ptr) {
41
+ rgame_recording_ref *ref = ptr;
42
+ rb_gc_mark(ref->app_object);
43
+ rb_gc_mark(ref->images);
44
+ }
45
+
46
+ static void recording_free(void *ptr) {
47
+ rgame_recording_ref *ref = ptr;
48
+ rgame_recording_free(ref->recording); /* NULL-safe */
49
+ xfree(ref);
50
+ }
51
+
52
+ static size_t recording_size(const void *ptr) {
53
+ (void)ptr;
54
+ return sizeof(rgame_recording_ref);
55
+ }
56
+
57
+ static const rb_data_type_t recording_data_type = {
58
+ .wrap_struct_name = "rgame_recording",
59
+ .function = {
60
+ .dmark = recording_mark,
61
+ .dfree = recording_free,
62
+ .dsize = recording_size,
63
+ },
64
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
65
+ };
66
+
67
+ static VALUE cRecording;
68
+
69
+ static rgame_recording_ref *recording_unwrap(VALUE self) {
70
+ rgame_recording_ref *ref;
71
+ TypedData_Get_Struct(self, rgame_recording_ref, &recording_data_type, ref);
72
+ if (!ref->recording) {
73
+ rb_raise(rb_eRuntimeError, "recording is not initialized");
74
+ }
75
+ return ref;
76
+ }
77
+
78
+ /*
79
+ * Declared even though `new` is refused below: wrapping a TypedData payload in
80
+ * a class that still has Object's allocator makes Ruby undefine it for us and
81
+ * warn about it. Saying so up front is one line and keeps the load quiet.
82
+ */
83
+ static VALUE recording_alloc(VALUE klass) {
84
+ rgame_recording_ref *ref;
85
+ VALUE object = TypedData_Make_Struct(klass, rgame_recording_ref, &recording_data_type, ref);
86
+ ref->recording = NULL;
87
+ ref->app = NULL;
88
+ ref->app_object = Qnil;
89
+ ref->images = Qnil;
90
+ return object;
91
+ }
92
+
93
+ VALUE rgame_recording_wrap(rgame_recording *recording, VALUE app_object, rgame_app *app,
94
+ VALUE images) {
95
+ rgame_recording_ref *ref;
96
+ VALUE object = TypedData_Make_Struct(cRecording, rgame_recording_ref, &recording_data_type,
97
+ ref);
98
+ ref->recording = recording;
99
+ ref->app = app;
100
+ ref->app_object = app_object;
101
+ ref->images = images;
102
+ return object;
103
+ }
104
+
105
+ /*
106
+ * #draw_at(x, y, z, rgba) — the raw form; RGame::Core::Recording#draw in
107
+ * lib/rgame/core/recording.rb is the one with keywords and colour coercion.
108
+ */
109
+ static VALUE recording_draw_at(VALUE self, VALUE x, VALUE y, VALUE z, VALUE color) {
110
+ rgame_recording_ref *ref = recording_unwrap(self);
111
+
112
+ /* The app that owns these textures has to be the one mid-frame. Anything
113
+ * else would append to a canvas nobody is about to submit, and draw
114
+ * nothing at all. */
115
+ if (!rgame_app_is_drawing(ref->app)) {
116
+ rb_raise(rb_eRuntimeError,
117
+ "drawing is only allowed inside #draw (the frame is not open)");
118
+ }
119
+
120
+ rgame_app_draw_recording(ref->app, ref->recording, (float)NUM2DBL(x), (float)NUM2DBL(y),
121
+ (unsigned int)(NUM2ULONG(color) & 0xFFFFFFFFul), NUM2DBL(z));
122
+ return self;
123
+ }
124
+
125
+ /* How many GL calls a replay costs — one per texture. Mostly for tests and for
126
+ * seeing whether a bake did what was hoped. */
127
+ static VALUE recording_batch_count(VALUE self) {
128
+ return UINT2NUM(rgame_recording_batch_count(recording_unwrap(self)->recording));
129
+ }
130
+
131
+ static VALUE recording_vertex_count(VALUE self) {
132
+ return UINT2NUM(rgame_recording_vertex_count(recording_unwrap(self)->recording));
133
+ }
134
+
135
+ static VALUE recording_empty_p(VALUE self) {
136
+ return rgame_recording_vertex_count(recording_unwrap(self)->recording) == 0 ? Qtrue : Qfalse;
137
+ }
138
+
139
+ /* The size of what was baked, so a game can skip replaying an off-screen
140
+ * layer. Measured in the recording's own coordinates. */
141
+ static VALUE recording_width(VALUE self) {
142
+ float min_x = 0.0f, max_x = 0.0f;
143
+ rgame_recording_bounds(recording_unwrap(self)->recording, &min_x, NULL, &max_x, NULL);
144
+ return DBL2NUM((double)(max_x - min_x));
145
+ }
146
+
147
+ static VALUE recording_height(VALUE self) {
148
+ float min_y = 0.0f, max_y = 0.0f;
149
+ rgame_recording_bounds(recording_unwrap(self)->recording, NULL, &min_y, NULL, &max_y);
150
+ return DBL2NUM((double)(max_y - min_y));
151
+ }
152
+
153
+ static VALUE recording_inspect(VALUE self) {
154
+ rgame_recording_ref *ref;
155
+ TypedData_Get_Struct(self, rgame_recording_ref, &recording_data_type, ref);
156
+ if (!ref->recording) {
157
+ return rb_sprintf("#<%" PRIsVALUE " (uninitialized)>", rb_obj_class(self));
158
+ }
159
+
160
+ return rb_sprintf("#<%" PRIsVALUE " %u batches, %u vertices>", rb_obj_class(self),
161
+ rgame_recording_batch_count(ref->recording),
162
+ rgame_recording_vertex_count(ref->recording));
163
+ }
164
+
165
+ /* Recordings come out of Renderer#record, so `new` is not part of the API —
166
+ * there is nothing a caller could pass that would make a valid one. */
167
+ static VALUE recording_s_new(int argc, VALUE *argv, VALUE klass) {
168
+ (void)argc;
169
+ (void)argv;
170
+ rb_raise(rb_eNoMethodError, "%" PRIsVALUE " is created by Renderer#record, not by .new",
171
+ klass);
172
+ }
173
+
174
+ void rgame_init_recording(VALUE mCore) {
175
+ cRecording = rb_define_class_under(mCore, "Recording", rb_cObject);
176
+
177
+ rb_define_alloc_func(cRecording, recording_alloc);
178
+ rb_define_singleton_method(cRecording, "new", recording_s_new, -1);
179
+ rb_define_method(cRecording, "draw_at", recording_draw_at, 4);
180
+ rb_define_method(cRecording, "batch_count", recording_batch_count, 0);
181
+ rb_define_method(cRecording, "vertex_count", recording_vertex_count, 0);
182
+ rb_define_method(cRecording, "empty?", recording_empty_p, 0);
183
+ rb_define_method(cRecording, "width", recording_width, 0);
184
+ rb_define_method(cRecording, "height", recording_height, 0);
185
+ rb_define_method(cRecording, "inspect", recording_inspect, 0);
186
+ }