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,304 @@
1
+ /*
2
+ * image.c — decode a PNG, upload it to the GPU, hand back a handle.
3
+ *
4
+ * This is "layer 3" in CLAUDE.md's abstraction strategy: the thin shim where
5
+ * real calls happen. Everything interesting about images — which rectangle of
6
+ * a sheet a sprite covers, how that becomes texture coordinates, when the
7
+ * shared upload may be deleted — is in texture.{c,h}, is pure, and is covered
8
+ * by test/test_texture.c with no GPU in sight. What is left here is a file
9
+ * read, one stb call and four GL calls, and it is kept this thin precisely so
10
+ * that "we don't unit-test it directly" is an honest position rather than a
11
+ * gap. `spec_core/rgame/core/image_spec.rb` exercises it end to end against a
12
+ * real GL context under Xvfb.
13
+ *
14
+ * ---------------------------------------------------------------------------
15
+ * What a handle is
16
+ * ---------------------------------------------------------------------------
17
+ *
18
+ * `struct rgame_image` is one `rgame_texture` view on the heap, plus the app it
19
+ * was uploaded into. The heap part exists only so the public API can be an
20
+ * opaque pointer; the view itself is a small value. Subimages and tiles
21
+ * allocate another one of these pointing at the same sheet, which is why
22
+ * slicing is cheap.
23
+ *
24
+ * The app pointer is carried because deleting a GL texture, like creating one,
25
+ * acts on whatever context is current — so freeing an image has to say which
26
+ * context it means. Each handle *retains* the app struct (not the window) for
27
+ * as long as it lives, which is what makes that pointer safe to follow even
28
+ * when the app was destroyed first. In that case the context is already gone,
29
+ * `rgame_app_gl_make_current` says so, and the deletion is skipped — correctly,
30
+ * because destroying a GL context frees every texture in it.
31
+ *
32
+ * That ordering is not hypothetical: Ruby can collect an app and its images in
33
+ * the same pass, and nothing specifies which is swept first.
34
+ */
35
+
36
+ #include "rgame/core.h"
37
+
38
+ #include "app/app_gl.h"
39
+ #include "graphics/texture.h"
40
+ #include "vendor/stb_image.h"
41
+
42
+ #include <SDL2/SDL_opengl.h>
43
+ #include <stdio.h>
44
+ #include <stdlib.h>
45
+
46
+ struct rgame_image {
47
+ rgame_app *app; /* borrowed — the app owns the context these pixels live in */
48
+ rgame_texture view;
49
+ };
50
+
51
+ /* Writes a message into the caller's buffer, if they asked for one. Every
52
+ * failure path below goes through this so none of them can forget. */
53
+ static void set_error(char *err, size_t err_size, const char *format, const char *detail) {
54
+ if (err && err_size > 0) {
55
+ snprintf(err, err_size, format, detail);
56
+ }
57
+ }
58
+
59
+ /*
60
+ * Reads a whole file into a malloc'd buffer.
61
+ *
62
+ * stb is built with STBI_NO_STDIO (see stb_image_impl.c) so that reading the
63
+ * file is our decision rather than the decoder's: one place decides how a
64
+ * missing file is reported, and the same code path will serve data that never
65
+ * was a file — an archive entry, or an asset baked into the gem.
66
+ */
67
+ static unsigned char *read_whole_file(const char *path, long *out_size) {
68
+ FILE *file = fopen(path, "rb");
69
+ if (!file) {
70
+ return NULL;
71
+ }
72
+
73
+ /* Seek to the end and ask where we are: the portable way to get a file's
74
+ * size without a stat() that differs per platform. */
75
+ if (fseek(file, 0, SEEK_END) != 0) {
76
+ fclose(file);
77
+ return NULL;
78
+ }
79
+ long size = ftell(file);
80
+ if (size <= 0) {
81
+ fclose(file);
82
+ return NULL;
83
+ }
84
+ rewind(file);
85
+
86
+ unsigned char *bytes = malloc((size_t)size);
87
+ if (!bytes) {
88
+ fclose(file);
89
+ return NULL;
90
+ }
91
+
92
+ /* A short read means a truncated or vanishing file; treat it as a failure
93
+ * rather than handing the decoder a partly-filled buffer. */
94
+ size_t got = fread(bytes, 1, (size_t)size, file);
95
+ fclose(file);
96
+ if (got != (size_t)size) {
97
+ free(bytes);
98
+ return NULL;
99
+ }
100
+
101
+ *out_size = size;
102
+ return bytes;
103
+ }
104
+
105
+ /*
106
+ * Uploads RGBA8 pixels and returns the GL texture name, or 0 on failure.
107
+ *
108
+ * `pixels` is tightly packed, top row first — which is the order stb decodes
109
+ * in, and the order texture.c's UVs assume (v increases downwards). Uploading
110
+ * bottom-up instead is the classic way every sprite ends up mirrored.
111
+ */
112
+ static unsigned int upload_rgba(const unsigned char *pixels, int width, int height) {
113
+ unsigned int name = 0;
114
+ glGenTextures(1, &name);
115
+ if (name == 0) {
116
+ return 0;
117
+ }
118
+
119
+ glBindTexture(GL_TEXTURE_2D, name);
120
+
121
+ /* Rows are 4 bytes per pixel so they are already 4-byte aligned, but GL's
122
+ * default alignment of 4 is a trap waiting for the day something uploads
123
+ * 3-byte or 1-byte pixels; saying 1 makes the upload independent of it. */
124
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
125
+
126
+ /* Nearest-neighbour, always: this engine draws pixel art, and there is no
127
+ * case where blurring it on scale-up is what was wanted. */
128
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
129
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
130
+
131
+ /* Clamp rather than repeat. Sprites come from a shared sheet, so a UV that
132
+ * slips a hair past an edge should pick up that edge's own pixel — with
133
+ * GL_REPEAT it wraps to the far side of the *sheet* and paints a stripe of
134
+ * an unrelated sprite. */
135
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
136
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
137
+
138
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
139
+ glBindTexture(GL_TEXTURE_2D, 0);
140
+
141
+ return name;
142
+ }
143
+
144
+ /* Wraps a view in a handle, releasing the view again if the allocation fails —
145
+ * so a failed subimage does not strand a reference on the sheet forever. */
146
+ static rgame_image *wrap(rgame_app *app, rgame_texture view) {
147
+ rgame_image *image = malloc(sizeof(rgame_image));
148
+ if (!image) {
149
+ unsigned int orphan = 0;
150
+ if (rgame_texture_destroy(&view, &orphan)) {
151
+ rgame_gl_context_save saved;
152
+ if (rgame_app_gl_make_current(app, &saved)) {
153
+ glDeleteTextures(1, &orphan);
154
+ }
155
+ rgame_app_gl_restore(&saved);
156
+ }
157
+ return NULL;
158
+ }
159
+
160
+ rgame_app_gl_retain(app);
161
+ image->app = app;
162
+ image->view = view;
163
+ return image;
164
+ }
165
+
166
+ rgame_image *rgame_image_load(rgame_app *app, const char *path, char *err, size_t err_size) {
167
+ if (!app || !path) {
168
+ set_error(err, err_size, "%s", "no app or path given");
169
+ return NULL;
170
+ }
171
+
172
+ /* Loading is allowed mid-frame, and a game with two windows may well be
173
+ * mid-frame in the *other* one, so whatever was current goes back at every
174
+ * exit below. */
175
+ rgame_gl_context_save saved;
176
+ if (!rgame_app_gl_make_current(app, &saved)) {
177
+ rgame_app_gl_restore(&saved);
178
+ set_error(err, err_size, "%s", "could not make the app's GL context current");
179
+ return NULL;
180
+ }
181
+
182
+ long size = 0;
183
+ unsigned char *bytes = read_whole_file(path, &size);
184
+ if (!bytes) {
185
+ rgame_app_gl_restore(&saved);
186
+ set_error(err, err_size, "could not read %s", path);
187
+ return NULL;
188
+ }
189
+
190
+ /* The trailing 4 asks stb for four channels whatever the file has, so a
191
+ * greyscale or palette PNG arrives as RGBA too and the upload below has
192
+ * exactly one format to handle. */
193
+ int width = 0, height = 0, channels_in_file = 0;
194
+ unsigned char *pixels = stbi_load_from_memory(bytes, (int)size, &width, &height,
195
+ &channels_in_file, 4);
196
+ free(bytes);
197
+ if (!pixels) {
198
+ rgame_app_gl_restore(&saved);
199
+ set_error(err, err_size, "could not decode %s", path);
200
+ return NULL;
201
+ }
202
+
203
+ unsigned int name = upload_rgba(pixels, width, height);
204
+ stbi_image_free(pixels);
205
+ if (name == 0) {
206
+ rgame_app_gl_restore(&saved);
207
+ set_error(err, err_size, "%s", "GL refused to allocate a texture");
208
+ return NULL;
209
+ }
210
+
211
+ rgame_texture_sheet *sheet = rgame_texture_sheet_create(name, width, height);
212
+ if (!sheet) {
213
+ glDeleteTextures(1, &name);
214
+ rgame_app_gl_restore(&saved);
215
+ set_error(err, err_size, "%s", "out of memory");
216
+ return NULL;
217
+ }
218
+
219
+ /* The sheet is created with one reference and the view takes a second, so
220
+ * hand back the creation reference: from here the handles own it. */
221
+ rgame_texture view = rgame_texture_whole(sheet);
222
+ rgame_texture_sheet_release(sheet, NULL);
223
+
224
+ rgame_image *image = wrap(app, view);
225
+ rgame_app_gl_restore(&saved);
226
+ if (!image) {
227
+ set_error(err, err_size, "%s", "out of memory");
228
+ }
229
+ return image;
230
+ }
231
+
232
+ rgame_image *rgame_image_subimage(const rgame_image *image, int x, int y, int width, int height) {
233
+ if (!image) {
234
+ return NULL;
235
+ }
236
+
237
+ rgame_texture view = {0};
238
+ if (!rgame_texture_subimage(&image->view, x, y, width, height, &view)) {
239
+ return NULL;
240
+ }
241
+
242
+ return wrap(image->app, view);
243
+ }
244
+
245
+ int rgame_image_tile_count(const rgame_image *image, int tile_width, int tile_height) {
246
+ return image ? rgame_texture_tile_count(&image->view, tile_width, tile_height) : 0;
247
+ }
248
+
249
+ rgame_image *rgame_image_tile(const rgame_image *image, int tile_width, int tile_height,
250
+ int index) {
251
+ if (!image) {
252
+ return NULL;
253
+ }
254
+
255
+ rgame_texture view = {0};
256
+ if (!rgame_texture_tile(&image->view, tile_width, tile_height, index, &view)) {
257
+ return NULL;
258
+ }
259
+
260
+ return wrap(image->app, view);
261
+ }
262
+
263
+ int rgame_image_width(const rgame_image *image) {
264
+ return image ? rgame_texture_width(&image->view) : 0;
265
+ }
266
+
267
+ int rgame_image_height(const rgame_image *image) {
268
+ return image ? rgame_texture_height(&image->view) : 0;
269
+ }
270
+
271
+ const rgame_texture *rgame_image_view(const rgame_image *image) {
272
+ return image ? &image->view : NULL;
273
+ }
274
+
275
+ rgame_app *rgame_image_owner(const rgame_image *image) {
276
+ return image ? image->app : NULL;
277
+ }
278
+
279
+ void rgame_image_destroy(rgame_image *image) {
280
+ if (!image) {
281
+ return;
282
+ }
283
+
284
+ /* Only the *last* handle on a sheet gets a name back to delete, which is
285
+ * how dropping a sprite sheet while its tiles are still alive stays safe.
286
+ *
287
+ * make_current fails when the app was destroyed first, and skipping the
288
+ * deletion is then the right answer rather than a leak: tearing down a GL
289
+ * context takes its textures with it. */
290
+ unsigned int name = 0;
291
+ if (rgame_texture_destroy(&image->view, &name)) {
292
+ rgame_gl_context_save saved;
293
+ if (rgame_app_gl_make_current(image->app, &saved)) {
294
+ glDeleteTextures(1, &name);
295
+ }
296
+ /* Always restored, including when the switch failed: a collector runs
297
+ * this at an arbitrary moment, quite possibly in the middle of another
298
+ * window's frame, and that frame still has to be submitted into its own
299
+ * context. */
300
+ rgame_app_gl_restore(&saved);
301
+ }
302
+ rgame_app_gl_release(image->app);
303
+ free(image);
304
+ }
@@ -0,0 +1,30 @@
1
+ #ifndef RGAME_IMAGE_INTERNAL_H
2
+ #define RGAME_IMAGE_INTERNAL_H
3
+
4
+ #include "rgame/core.h"
5
+ #include "graphics/texture.h"
6
+
7
+ /*
8
+ * The texture view inside an image handle.
9
+ *
10
+ * `rgame_image` is opaque in the public header, and stays that way: a caller
11
+ * outside this extension has no business knowing what a texture view is. But
12
+ * the drawing path does — app.c has to hand the view to primitives.c — so the
13
+ * accessor lives here, in a header that is not under include/ and therefore
14
+ * cannot leave ext/rgame_core/.
15
+ *
16
+ * Returns NULL for a NULL image. The view belongs to the image; it must not be
17
+ * destroyed by the caller.
18
+ */
19
+ const rgame_texture *rgame_image_view(const rgame_image *image);
20
+
21
+ /*
22
+ * The app whose GL context this image was uploaded into, or NULL.
23
+ *
24
+ * Textures are not shared between contexts, so drawing an image through a
25
+ * *different* app samples nothing and paints a plain white quad — no GL error,
26
+ * no clue. The draw path compares owners so that this can be reported instead.
27
+ */
28
+ rgame_app *rgame_image_owner(const rgame_image *image);
29
+
30
+ #endif /* RGAME_IMAGE_INTERNAL_H */
@@ -0,0 +1,189 @@
1
+ /*
2
+ * primitives.c — rectangles, lines, circles and images, in terms of the
3
+ * canvas's triangles and quads. Pure; see primitives.h.
4
+ */
5
+
6
+ #include "graphics/primitives.h"
7
+
8
+ #include <math.h>
9
+
10
+ /* math.h only promises M_PI under POSIX, and -std=c17 asks for strict ISO C,
11
+ * which hides it. Spelling it out here is one line and portable. */
12
+ #define RGAME_TWO_PI 6.283185307179586f
13
+
14
+ void rgame_prim_rect(rgame_canvas *canvas, float x, float y, float width, float height,
15
+ rgame_color color, double z) {
16
+ float xy8[8] = {
17
+ x, y,
18
+ x + width, y,
19
+ x + width, y + height,
20
+ x, y + height,
21
+ };
22
+ rgame_canvas_quad(canvas, xy8, color, z);
23
+ }
24
+
25
+ void rgame_prim_line(rgame_canvas *canvas, float x1, float y1, float x2, float y2,
26
+ float thickness, rgame_color color, double z) {
27
+ float dx = x2 - x1;
28
+ float dy = y2 - y1;
29
+ float length = sqrtf((dx * dx) + (dy * dy));
30
+ if (length == 0.0f) {
31
+ return;
32
+ }
33
+
34
+ /* Half the thickness, perpendicular to the direction of travel. Rotating a
35
+ * vector by 90 degrees swaps its components and negates one, which is where
36
+ * (dy, dx) comes from — and why the two corners on each end use opposite
37
+ * signs. */
38
+ float scale = (thickness / 2.0f) / length;
39
+ float ox = dy * scale;
40
+ float oy = dx * scale;
41
+
42
+ /* Loop order around the quad: the two corners at the start, then the two at
43
+ * the end, coming back along the other side. Listing them 1a, 2a, 2b, 1b
44
+ * (rather than 1a, 1b, 2a, 2b) is what keeps it a rectangle instead of an
45
+ * hourglass. */
46
+ float xy8[8] = {
47
+ x1 - ox, y1 + oy,
48
+ x2 - ox, y2 + oy,
49
+ x2 + ox, y2 - oy,
50
+ x1 + ox, y1 - oy,
51
+ };
52
+ rgame_canvas_quad(canvas, xy8, color, z);
53
+ }
54
+
55
+ void rgame_prim_circle(rgame_canvas *canvas, float cx, float cy, float radius, int segments,
56
+ rgame_color color, double z) {
57
+ if (segments < 3 || radius <= 0.0f) {
58
+ return;
59
+ }
60
+
61
+ float step = RGAME_TWO_PI / (float)segments;
62
+ for (int i = 0; i < segments; i++) {
63
+ float a0 = step * (float)i;
64
+ float a1 = step * (float)(i + 1);
65
+
66
+ /* Every wedge starts at the centre, so the fan closes exactly: the last
67
+ * wedge's far edge is computed from segment count, not accumulated, and
68
+ * cannot drift into a visible gap. */
69
+ float xy6[6] = {
70
+ cx, cy,
71
+ cx + (cosf(a0) * radius), cy + (sinf(a0) * radius),
72
+ cx + (cosf(a1) * radius), cy + (sinf(a1) * radius),
73
+ };
74
+ rgame_canvas_triangle(canvas, xy6, color, z);
75
+ }
76
+ }
77
+
78
+ /*
79
+ * The four corners of `width` x `height` at (x, y), plus the view's texture
80
+ * coordinates, in the one corner order everything here uses.
81
+ *
82
+ * `flip_x`/`flip_y` mirror the *coordinates*, not the geometry: the corners are
83
+ * in loop order — TL, TR, BR, BL — so a horizontal mirror exchanges the `u` of
84
+ * the two top corners and of the two bottom ones. Doing it here rather than by
85
+ * handing in a negative width is what keeps the drawn rectangle exactly where
86
+ * the caller put it.
87
+ */
88
+ static void textured_rect(rgame_canvas *canvas, const rgame_texture *view, float x, float y,
89
+ float width, float height, int flip_x, int flip_y, rgame_color color,
90
+ double z) {
91
+ float xy8[8] = {
92
+ x, y,
93
+ x + width, y,
94
+ x + width, y + height,
95
+ x, y + height,
96
+ };
97
+ float uv8[8];
98
+ rgame_texture_uv(view, uv8);
99
+
100
+ float swap;
101
+ if (flip_x) {
102
+ swap = uv8[0]; uv8[0] = uv8[2]; uv8[2] = swap; /* u of TL and TR */
103
+ swap = uv8[6]; uv8[6] = uv8[4]; uv8[4] = swap; /* u of BL and BR */
104
+ }
105
+ if (flip_y) {
106
+ swap = uv8[1]; uv8[1] = uv8[7]; uv8[7] = swap; /* v of TL and BL */
107
+ swap = uv8[3]; uv8[3] = uv8[5]; uv8[5] = swap; /* v of TR and BR */
108
+ }
109
+
110
+ rgame_canvas_textured_quad(canvas, rgame_texture_name(view), xy8, uv8, color, z);
111
+ }
112
+
113
+ void rgame_prim_glyph(rgame_canvas *canvas, unsigned int texture, rgame_rect source,
114
+ int page_width, int page_height, float x, float y, rgame_color color,
115
+ double z) {
116
+ if (source.w <= 0 || source.h <= 0 || page_width <= 0 || page_height <= 0) {
117
+ return;
118
+ }
119
+
120
+ float xy8[8] = {
121
+ x, y,
122
+ x + (float)source.w, y,
123
+ x + (float)source.w, y + (float)source.h,
124
+ x, y + (float)source.h,
125
+ };
126
+
127
+ /* Normalised against the page, not the glyph — the same division
128
+ * rgame_texture_uv does, for the same reason: dividing by the glyph's own
129
+ * size would stretch every letter across the whole page. */
130
+ float u0 = (float)source.x / (float)page_width;
131
+ float v0 = (float)source.y / (float)page_height;
132
+ float u1 = (float)(source.x + source.w) / (float)page_width;
133
+ float v1 = (float)(source.y + source.h) / (float)page_height;
134
+
135
+ float uv8[8] = { u0, v0, u1, v0, u1, v1, u0, v1 };
136
+
137
+ rgame_canvas_textured_quad(canvas, texture, xy8, uv8, color, z);
138
+ }
139
+
140
+ void rgame_prim_image(rgame_canvas *canvas, const rgame_texture *view, float x, float y,
141
+ rgame_color color, double z) {
142
+ rgame_prim_image_scaled(canvas, view, x, y, 1.0f, 1.0f, color, z);
143
+ }
144
+
145
+ void rgame_prim_image_scaled(rgame_canvas *canvas, const rgame_texture *view, float x, float y,
146
+ float scale_x, float scale_y, rgame_color color, double z) {
147
+ if (!view || !view->sheet || scale_x == 0.0f || scale_y == 0.0f) {
148
+ return;
149
+ }
150
+
151
+ /* The sign selects the mirror and the magnitude the size, so the rectangle
152
+ * is always well-formed and the caller never has to work out which corner
153
+ * the arithmetic landed on. */
154
+ textured_rect(canvas, view, x, y, (float)rgame_texture_width(view) * fabsf(scale_x),
155
+ (float)rgame_texture_height(view) * fabsf(scale_y), scale_x < 0.0f,
156
+ scale_y < 0.0f, color, z);
157
+ }
158
+
159
+ void rgame_prim_image_rot(rgame_canvas *canvas, const rgame_texture *view, float cx, float cy,
160
+ float angle_degrees, float scale, rgame_color color, double z) {
161
+ if (!view || !view->sheet) {
162
+ return;
163
+ }
164
+
165
+ float width = (float)rgame_texture_width(view);
166
+ float height = (float)rgame_texture_height(view);
167
+
168
+ /* The common case — a sprite drawn upright at its natural size — is one
169
+ * quad and no stack traffic at all. */
170
+ if (angle_degrees == 0.0f && scale == 1.0f) {
171
+ textured_rect(canvas, view, cx - (width / 2.0f), cy - (height / 2.0f), width, height, 0,
172
+ 0, color, z);
173
+ return;
174
+ }
175
+
176
+ /* Otherwise place the quad at the origin and move the *world* to it:
177
+ * translate to the centre, rotate about it, scale. Reusing the transform
178
+ * stack means the direction a positive angle turns is decided in exactly
179
+ * one place (transform.c), not re-derived here with a sign to get wrong. */
180
+ rgame_canvas_push_translate(canvas, cx, cy);
181
+ rgame_canvas_push_rotate(canvas, angle_degrees, 0.0f, 0.0f);
182
+ rgame_canvas_push_scale(canvas, scale, scale);
183
+
184
+ textured_rect(canvas, view, -width / 2.0f, -height / 2.0f, width, height, 0, 0, color, z);
185
+
186
+ rgame_canvas_pop(canvas);
187
+ rgame_canvas_pop(canvas);
188
+ rgame_canvas_pop(canvas);
189
+ }
@@ -0,0 +1,111 @@
1
+ #ifndef RGAME_PRIMITIVES_H
2
+ #define RGAME_PRIMITIVES_H
3
+
4
+ #include "graphics/canvas.h"
5
+ #include "color.h"
6
+ #include "graphics/texture.h"
7
+
8
+ /*
9
+ * The shapes a game actually asks for, expressed in the two the canvas knows.
10
+ *
11
+ * A canvas draws triangles and quads. A game draws rectangles, lines, circles
12
+ * and sprites. This module is the translation, and it is pure — every function
13
+ * here turns arguments into `rgame_canvas_*` calls and touches nothing else,
14
+ * so `test/test_primitives.c` can check what a circle produces by reading the
15
+ * draw queue, with no GPU and no window.
16
+ *
17
+ * It sits between the Ruby renderer and the canvas rather than inside either,
18
+ * for the usual reason: a line's corner arithmetic is exactly the kind of thing
19
+ * that is wrong by a factor of two and looks *almost* right, and it should be
20
+ * checkable without a display.
21
+ *
22
+ * ---------------------------------------------------------------------------
23
+ * Corner order
24
+ * ---------------------------------------------------------------------------
25
+ *
26
+ * Everything here hands the canvas its four points in loop order — top-left,
27
+ * top-right, bottom-right, bottom-left — matching `rgame_canvas_quad` and the
28
+ * UV order `rgame_texture_uv` writes. Keeping one order everywhere is what
29
+ * makes textured drawing work without a second convention to translate.
30
+ */
31
+
32
+ /* An axis-aligned filled rectangle. */
33
+ void rgame_prim_rect(rgame_canvas *canvas, float x, float y, float width, float height,
34
+ rgame_color color, double z);
35
+
36
+ /*
37
+ * A line of real thickness from (x1,y1) to (x2,y2), as a quad offset
38
+ * perpendicular to its direction. GL's own line width is a suggestion drivers
39
+ * are free to ignore beyond 1px, so a line worth seeing has to be a shape.
40
+ *
41
+ * A zero-length line draws nothing rather than dividing by zero.
42
+ */
43
+ void rgame_prim_line(rgame_canvas *canvas, float x1, float y1, float x2, float y2,
44
+ float thickness, rgame_color color, double z);
45
+
46
+ /*
47
+ * A filled circle as a fan of `segments` triangles around its centre.
48
+ *
49
+ * The layer being replaced cached a unit-circle *texture* and drew it scaled,
50
+ * because every triangle was a separate draw call there. With a batching queue
51
+ * the fan costs one batch, so the texture — and the render-to-texture support
52
+ * it needed — buys nothing. Fewer than 3 segments draws nothing.
53
+ */
54
+ void rgame_prim_circle(rgame_canvas *canvas, float cx, float cy, float radius, int segments,
55
+ rgame_color color, double z);
56
+
57
+ /*
58
+ * An image with its *top-left* at (x, y), at its natural size. This is the
59
+ * backdrop case: no rotation, no scale, nothing to decide.
60
+ */
61
+ void rgame_prim_image(rgame_canvas *canvas, const rgame_texture *view, float x, float y,
62
+ rgame_color color, double z);
63
+
64
+ /*
65
+ * An image with its top-left at (x, y), scaled independently per axis. The tile
66
+ * and sprite-frame case: a nine-slice corner drawn at 3x, a walk frame facing
67
+ * the other way.
68
+ *
69
+ * **A negative scale mirrors the image inside the same rectangle; it does not
70
+ * move it.** The drawn area is always (x, y) to (x + |w·scale_x|, y +
71
+ * |h·scale_y|), so the anchor means what it says whatever the sign — and
72
+ * `scale_x = -1` is "the same sprite, facing left", not "the same sprite, one
73
+ * width to the left". The layer being replaced had the other convention (Gosu
74
+ * mirrors *about* the anchor) and every caller of it compensated by adding a
75
+ * width back, which is a correction that is silently wrong the one time it is
76
+ * forgotten.
77
+ *
78
+ * A zero scale on either axis draws nothing rather than a degenerate quad.
79
+ */
80
+ void rgame_prim_image_scaled(rgame_canvas *canvas, const rgame_texture *view, float x, float y,
81
+ float scale_x, float scale_y, rgame_color color, double z);
82
+
83
+ /*
84
+ * An image *centred* on (cx, cy), rotated `angle_degrees` clockwise about that
85
+ * centre and uniformly scaled — the sprite case, and the shape the layer being
86
+ * replaced exposed as `draw_rot`.
87
+ *
88
+ * The rotation goes through the canvas's own transform stack rather than a
89
+ * second sin/cos here, so there is exactly one place in the engine that decides
90
+ * which way a positive angle turns. An unrotated, unscaled image skips the
91
+ * stack entirely and emits the quad directly.
92
+ */
93
+ void rgame_prim_image_rot(rgame_canvas *canvas, const rgame_texture *view, float cx, float cy,
94
+ float angle_degrees, float scale, rgame_color color, double z);
95
+
96
+ /*
97
+ * One glyph out of an atlas page, drawn 1:1 at (x, y).
98
+ *
99
+ * Separate from `rgame_prim_image` because a glyph's source is a rectangle of a
100
+ * shared page rather than a texture view — the page has no `rgame_texture`
101
+ * wrapped around it, and giving it one would mean a refcounted sheet per font
102
+ * page for no benefit. `page_width` and `page_height` are what the source rect
103
+ * is normalised against.
104
+ *
105
+ * A source rect with no pixels (a space) draws nothing.
106
+ */
107
+ void rgame_prim_glyph(rgame_canvas *canvas, unsigned int texture, rgame_rect source,
108
+ int page_width, int page_height, float x, float y, rgame_color color,
109
+ double z);
110
+
111
+ #endif /* RGAME_PRIMITIVES_H */