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,119 @@
1
+ /*
2
+ * recording.c — copying a prepared frame out of the queue and keeping it.
3
+ * See recording.h for what a recording is for and what it does not capture.
4
+ */
5
+
6
+ #include "graphics/recording.h"
7
+
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+
11
+ void rgame_recording_destroy(rgame_recording *recording) {
12
+ if (!recording) {
13
+ return;
14
+ }
15
+
16
+ free(recording->vertices);
17
+ free(recording->batches);
18
+ /* Zeroed rather than merely freed, so a second destroy is a no-op and a
19
+ * replay after destroy draws nothing instead of reading freed memory. */
20
+ recording->vertices = NULL;
21
+ recording->batches = NULL;
22
+ recording->vertex_count = 0;
23
+ recording->batch_count = 0;
24
+ }
25
+
26
+ int rgame_recording_capture(rgame_recording *out, const rgame_draw_queue *queue) {
27
+ if (!out || !queue) {
28
+ return 0;
29
+ }
30
+
31
+ out->vertices = NULL;
32
+ out->batches = NULL;
33
+ out->vertex_count = 0;
34
+ out->batch_count = 0;
35
+
36
+ unsigned int batch_count = rgame_draw_queue_batch_count(queue);
37
+ unsigned int vertex_count = rgame_draw_queue_vertex_count(queue);
38
+ if (batch_count == 0 || vertex_count == 0) {
39
+ return 1; /* nothing was drawn; an empty recording is a valid one */
40
+ }
41
+
42
+ /* Exact allocations, not the queue's growth strategy: a recording is
43
+ * written once and read for the rest of the level, so the spare capacity
44
+ * that makes a per-frame queue cheap would just be a level-long leak. */
45
+ rgame_vertex *vertices = malloc(sizeof(rgame_vertex) * vertex_count);
46
+ rgame_recording_batch *batches = malloc(sizeof(rgame_recording_batch) * batch_count);
47
+ if (!vertices || !batches) {
48
+ free(vertices);
49
+ free(batches);
50
+ return 0;
51
+ }
52
+
53
+ memcpy(vertices, rgame_draw_queue_vertices(queue), sizeof(rgame_vertex) * vertex_count);
54
+
55
+ /*
56
+ * The queue's batches carry a clip as well; only the texture and the span
57
+ * come across. See recording.h — a clip recorded at one place on screen is
58
+ * wrong at every other place the recording is replayed.
59
+ */
60
+ for (unsigned int i = 0; i < batch_count; i++) {
61
+ const rgame_draw_batch *source = rgame_draw_queue_batch(queue, i);
62
+ batches[i].texture = source->texture;
63
+ batches[i].first_vertex = source->first_vertex;
64
+ batches[i].vertex_count = source->vertex_count;
65
+ }
66
+
67
+ out->vertices = vertices;
68
+ out->vertex_count = vertex_count;
69
+ out->batches = batches;
70
+ out->batch_count = batch_count;
71
+ return 1;
72
+ }
73
+
74
+ unsigned int rgame_recording_batch_count(const rgame_recording *recording) {
75
+ return recording ? recording->batch_count : 0;
76
+ }
77
+
78
+ unsigned int rgame_recording_vertex_count(const rgame_recording *recording) {
79
+ return recording ? recording->vertex_count : 0;
80
+ }
81
+
82
+ void rgame_recording_bounds(const rgame_recording *recording, float *min_x, float *min_y,
83
+ float *max_x, float *max_y) {
84
+ float left = 0.0f, top = 0.0f, right = 0.0f, bottom = 0.0f;
85
+
86
+ if (recording && recording->vertex_count > 0) {
87
+ left = right = recording->vertices[0].x;
88
+ top = bottom = recording->vertices[0].y;
89
+
90
+ for (unsigned int i = 1; i < recording->vertex_count; i++) {
91
+ const rgame_vertex *vertex = &recording->vertices[i];
92
+ if (vertex->x < left) {
93
+ left = vertex->x;
94
+ }
95
+ if (vertex->x > right) {
96
+ right = vertex->x;
97
+ }
98
+ if (vertex->y < top) {
99
+ top = vertex->y;
100
+ }
101
+ if (vertex->y > bottom) {
102
+ bottom = vertex->y;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (min_x) {
108
+ *min_x = left;
109
+ }
110
+ if (min_y) {
111
+ *min_y = top;
112
+ }
113
+ if (max_x) {
114
+ *max_x = right;
115
+ }
116
+ if (max_y) {
117
+ *max_y = bottom;
118
+ }
119
+ }
@@ -0,0 +1,88 @@
1
+ #ifndef RGAME_RECORDING_H
2
+ #define RGAME_RECORDING_H
3
+
4
+ #include "graphics/draw_queue.h"
5
+
6
+ /*
7
+ * A baked block of drawing, kept between frames and replayed as a few calls.
8
+ *
9
+ * The problem it solves is the tile map. A screen of 16px tiles is a couple of
10
+ * thousand quads, and while the batching queue already merges them into one GL
11
+ * call, the *CPU* still walks every tile, transforms four corners and appends a
12
+ * command — sixty times a second, for a layer that has not changed since the
13
+ * level loaded.
14
+ *
15
+ * A recording captures that work once. What comes out is the finished vertex
16
+ * array, already sorted and grouped by texture, so replaying it is one command
17
+ * per texture instead of one per tile:
18
+ *
19
+ * bake once 2000 tiles -> 2000 commands -> 1 batch
20
+ * replay 1 command -> 1 batch, every frame
21
+ *
22
+ * ---------------------------------------------------------------------------
23
+ * What is captured, and what is not
24
+ * ---------------------------------------------------------------------------
25
+ *
26
+ * Positions and texture coordinates are baked. Transforms *inside* the recorded
27
+ * block are baked with them — a rotated sprite records its rotated corners —
28
+ * and the transform in effect at *replay* time is applied on top, which is what
29
+ * lets a baked layer scroll under a camera.
30
+ *
31
+ * Clips are not captured at all: a batch carries a texture and a span of
32
+ * vertices, and nothing else. Clipping happens at rasterisation, so it cannot
33
+ * be baked into a vertex, and a clip rectangle recorded in one place would be
34
+ * wrong everywhere the recording is later replayed. Pushing a clip inside a
35
+ * recorded block is therefore refused rather than silently dropped; clip the
36
+ * *replay* instead, which does exactly what the caller meant.
37
+ *
38
+ * Colours are baked, but a replay may tint them — multiplying each recorded
39
+ * component by the tint's. The layer being replaced could not do this (its
40
+ * recorded images drew white-only, and callers worked around it); there is no
41
+ * reason to inherit the limitation.
42
+ */
43
+
44
+ /* A run of recorded vertices sharing one texture — one GL call's worth. */
45
+ typedef struct {
46
+ unsigned int texture; /* 0 means untextured */
47
+ unsigned int first_vertex, vertex_count;
48
+ } rgame_recording_batch;
49
+
50
+ /* Tagged so the public header can forward-declare the same type without
51
+ * seeing what is in it — src/main.c and the Ruby binding only ever hold a
52
+ * pointer. */
53
+ typedef struct rgame_recording {
54
+ rgame_vertex *vertices; /* in painter order, grouped by batch */
55
+ unsigned int vertex_count;
56
+
57
+ rgame_recording_batch *batches;
58
+ unsigned int batch_count;
59
+ } rgame_recording;
60
+
61
+ /*
62
+ * Copies a *prepared* draw queue into `out`, which must be uninitialised or
63
+ * already destroyed. Returns 1 on success, 0 if out of memory — in which case
64
+ * `out` is left empty and is still safe to destroy.
65
+ *
66
+ * "Prepared" means rgame_draw_queue_prepare has run: the sorting and grouping
67
+ * is exactly the work being saved, so a recording captures its result rather
68
+ * than the raw command list.
69
+ *
70
+ * An empty queue records successfully and replays as nothing.
71
+ */
72
+ int rgame_recording_capture(rgame_recording *out, const rgame_draw_queue *queue);
73
+
74
+ /* Frees the buffers. Safe on a zeroed struct, and safe to call twice. */
75
+ void rgame_recording_destroy(rgame_recording *recording);
76
+
77
+ unsigned int rgame_recording_batch_count(const rgame_recording *recording);
78
+ unsigned int rgame_recording_vertex_count(const rgame_recording *recording);
79
+
80
+ /*
81
+ * The bounding box of everything recorded, in the recording's own coordinates.
82
+ * Zero-sized for an empty recording. Games use it to decide whether a baked
83
+ * layer is on screen at all before replaying it.
84
+ */
85
+ void rgame_recording_bounds(const rgame_recording *recording, float *min_x, float *min_y,
86
+ float *max_x, float *max_y);
87
+
88
+ #endif /* RGAME_RECORDING_H */
@@ -0,0 +1,181 @@
1
+ /*
2
+ * texture.c — sheet refcounting, sub-rectangle composition, pixel-to-UV.
3
+ * See texture.h for what the two structs are and why the refcount lives here.
4
+ */
5
+
6
+ #include "graphics/texture.h"
7
+
8
+ #include <stdlib.h>
9
+
10
+ /* See rgame_texture_live_sheets in the header for why this exists. */
11
+ static long live_sheets = 0;
12
+
13
+ long rgame_texture_live_sheets(void) {
14
+ return live_sheets;
15
+ }
16
+
17
+ rgame_texture_sheet *rgame_texture_sheet_create(unsigned int name, int width, int height) {
18
+ if (width <= 0 || height <= 0) {
19
+ return NULL;
20
+ }
21
+
22
+ rgame_texture_sheet *sheet = malloc(sizeof(rgame_texture_sheet));
23
+ if (!sheet) {
24
+ return NULL;
25
+ }
26
+
27
+ sheet->name = name;
28
+ sheet->width = width;
29
+ sheet->height = height;
30
+ sheet->refs = 1;
31
+ live_sheets++;
32
+ return sheet;
33
+ }
34
+
35
+ void rgame_texture_sheet_retain(rgame_texture_sheet *sheet) {
36
+ if (sheet) {
37
+ sheet->refs++;
38
+ }
39
+ }
40
+
41
+ int rgame_texture_sheet_release(rgame_texture_sheet *sheet, unsigned int *out_name) {
42
+ if (!sheet) {
43
+ return 0;
44
+ }
45
+
46
+ if (--sheet->refs > 0) {
47
+ return 0;
48
+ }
49
+
50
+ /* Read the name out before freeing — after free(sheet) it is gone, and
51
+ * reading it back is the classic use-after-free this ordering avoids. */
52
+ unsigned int name = sheet->name;
53
+ free(sheet);
54
+ live_sheets--;
55
+ if (out_name) {
56
+ *out_name = name;
57
+ }
58
+ return 1;
59
+ }
60
+
61
+ rgame_texture rgame_texture_whole(rgame_texture_sheet *sheet) {
62
+ rgame_texture view = {0};
63
+ if (!sheet) {
64
+ return view;
65
+ }
66
+
67
+ rgame_texture_sheet_retain(sheet);
68
+ view.sheet = sheet;
69
+ view.rect = rgame_rect_make(0, 0, sheet->width, sheet->height);
70
+ return view;
71
+ }
72
+
73
+ int rgame_texture_destroy(rgame_texture *view, unsigned int *out_name) {
74
+ if (!view || !view->sheet) {
75
+ return 0;
76
+ }
77
+
78
+ int died = rgame_texture_sheet_release(view->sheet, out_name);
79
+ /* Clearing the pointer makes a second destroy a no-op rather than a double
80
+ * free. Ruby's GC and an explicit close can both reach the same view. */
81
+ view->sheet = NULL;
82
+ view->rect = rgame_rect_make(0, 0, 0, 0);
83
+ return died;
84
+ }
85
+
86
+ rgame_texture rgame_texture_clone(const rgame_texture *view) {
87
+ rgame_texture copy = {0};
88
+ if (!view || !view->sheet) {
89
+ return copy;
90
+ }
91
+
92
+ rgame_texture_sheet_retain(view->sheet);
93
+ copy = *view;
94
+ return copy;
95
+ }
96
+
97
+ int rgame_texture_subimage(const rgame_texture *view, int x, int y, int w, int h,
98
+ rgame_texture *out) {
99
+ if (!view || !view->sheet || !out || w <= 0 || h <= 0 || x < 0 || y < 0) {
100
+ return 0;
101
+ }
102
+
103
+ /* Compared against the *view's* size, not the sheet's: a sub-rect is
104
+ * relative to what it is being cut from, so a tile of a tile can never
105
+ * wander outside the tile it came from. */
106
+ if (x + w > view->rect.w || y + h > view->rect.h) {
107
+ return 0;
108
+ }
109
+
110
+ rgame_texture_sheet_retain(view->sheet);
111
+ out->sheet = view->sheet;
112
+ out->rect = rgame_rect_make(view->rect.x + x, view->rect.y + y, w, h);
113
+ return 1;
114
+ }
115
+
116
+ unsigned int rgame_texture_name(const rgame_texture *view) {
117
+ return view && view->sheet ? view->sheet->name : 0;
118
+ }
119
+
120
+ int rgame_texture_width(const rgame_texture *view) {
121
+ return view && view->sheet ? view->rect.w : 0;
122
+ }
123
+
124
+ int rgame_texture_height(const rgame_texture *view) {
125
+ return view && view->sheet ? view->rect.h : 0;
126
+ }
127
+
128
+ int rgame_texture_tile_count(const rgame_texture *view, int tile_width, int tile_height) {
129
+ if (!view || !view->sheet || tile_width <= 0 || tile_height <= 0) {
130
+ return 0;
131
+ }
132
+
133
+ return (view->rect.w / tile_width) * (view->rect.h / tile_height);
134
+ }
135
+
136
+ int rgame_texture_tile(const rgame_texture *view, int tile_width, int tile_height, int index,
137
+ rgame_texture *out) {
138
+ /* `index < 0` is redundant today and deliberately kept: a negative index
139
+ * yields negative offsets, which rgame_texture_subimage refuses anyway, so
140
+ * mutating it away survives the suite. Half a range check is still the
141
+ * wrong thing to read at the place the range is computed. */
142
+ if (index < 0 || index >= rgame_texture_tile_count(view, tile_width, tile_height)) {
143
+ return 0;
144
+ }
145
+
146
+ int columns = view->rect.w / tile_width;
147
+ return rgame_texture_subimage(view, (index % columns) * tile_width,
148
+ (index / columns) * tile_height, tile_width, tile_height, out);
149
+ }
150
+
151
+ void rgame_texture_uv(const rgame_texture *view, float *uv8) {
152
+ if (!uv8) {
153
+ return;
154
+ }
155
+
156
+ /* A view with no sheet has no pixels to sample; hand back a degenerate
157
+ * 0,0 square rather than dividing by a zero sheet size. */
158
+ if (!view || !view->sheet) {
159
+ for (int i = 0; i < 8; i++) {
160
+ uv8[i] = 0.0f;
161
+ }
162
+ return;
163
+ }
164
+
165
+ /* Normalise against the *sheet*: UVs address the whole uploaded texture,
166
+ * so a 16px tile in a 512px sheet spans 1/32 of the coordinate space, not
167
+ * all of it. Dividing by the view size instead is the mistake this
168
+ * function exists to make impossible to repeat. */
169
+ float sheet_w = (float)view->sheet->width;
170
+ float sheet_h = (float)view->sheet->height;
171
+
172
+ float u0 = (float)view->rect.x / sheet_w;
173
+ float v0 = (float)view->rect.y / sheet_h;
174
+ float u1 = (float)(view->rect.x + view->rect.w) / sheet_w;
175
+ float v1 = (float)(view->rect.y + view->rect.h) / sheet_h;
176
+
177
+ uv8[0] = u0; uv8[1] = v0; /* top-left */
178
+ uv8[2] = u1; uv8[3] = v0; /* top-right */
179
+ uv8[4] = u1; uv8[5] = v1; /* bottom-right */
180
+ uv8[6] = u0; uv8[7] = v1; /* bottom-left */
181
+ }
@@ -0,0 +1,165 @@
1
+ #ifndef RGAME_TEXTURE_H
2
+ #define RGAME_TEXTURE_H
3
+
4
+ #include "graphics/clip.h"
5
+
6
+ /*
7
+ * Textures: what part of an uploaded image a draw call should sample, and who
8
+ * owns the upload. Pure arithmetic and pure bookkeeping — no GL, no stb, no
9
+ * I/O. The decode-and-upload half lives in image.c, which is deliberately as
10
+ * dumb as it can be (CLAUDE.md's layer 3).
11
+ *
12
+ * ---------------------------------------------------------------------------
13
+ * Two structs, because there are two lifetimes
14
+ * ---------------------------------------------------------------------------
15
+ *
16
+ * A sprite sheet is decoded and uploaded to the GPU *once*, and then sliced
17
+ * into dozens of sprites. Each of those sprites is an ordinary object a game
18
+ * holds, drops and garbage-collects on its own schedule — but the pixels
19
+ * behind them are one shared allocation on the GPU.
20
+ *
21
+ * So:
22
+ *
23
+ * rgame_texture_sheet one GL texture. Reference-counted, heap-allocated,
24
+ * shared by every view into it.
25
+ * rgame_texture a *view*: a sheet plus the sub-rectangle of it this
26
+ * particular sprite occupies. Small, copyable, holds
27
+ * one reference to its sheet.
28
+ *
29
+ * `subimage` therefore costs a rectangle intersection and a refcount bump —
30
+ * no second decode and no second upload. Slicing a 512x512 sheet into 1024
31
+ * tiles produces 1024 views over one texture.
32
+ *
33
+ * ---------------------------------------------------------------------------
34
+ * Why the refcount is here and not in image.c
35
+ * ---------------------------------------------------------------------------
36
+ *
37
+ * "Free the GPU texture exactly when the last sprite using it goes away" is
38
+ * the kind of rule that is wrong in one branch and leaks for a year. It is
39
+ * also, written out, pure integer bookkeeping — nothing about it needs a GPU.
40
+ * So it lives here, where `test/test_texture.c` can drop views in every order
41
+ * and assert the sheet dies once and only once.
42
+ *
43
+ * The one thing this module cannot do is call `glDeleteTextures`. Instead
44
+ * `rgame_texture_sheet_release` *reports* that the last reference went away
45
+ * and hands back the GL name it was holding; the caller in image.c does the
46
+ * one-line deletion. That keeps the decision here and the syscall there.
47
+ *
48
+ * ---------------------------------------------------------------------------
49
+ * Pixels in, UVs out
50
+ * ---------------------------------------------------------------------------
51
+ *
52
+ * Callers talk in pixels — "the tile at 32,16, sixteen by sixteen" — because
53
+ * that is how sprite sheets are drawn and described. OpenGL samples in
54
+ * normalised texture coordinates, 0..1 across the whole texture. Converting
55
+ * between them is one division that is easy to get subtly wrong (by the sheet
56
+ * size, not the view size), so it happens in exactly one place:
57
+ * `rgame_texture_uv`.
58
+ */
59
+
60
+ typedef struct {
61
+ /* The GL texture name. Opaque here: this module never calls GL, it only
62
+ * carries the number so that release can hand it back for deletion. */
63
+ unsigned int name;
64
+ int width, height; /* the whole uploaded image, in pixels */
65
+ int refs;
66
+ } rgame_texture_sheet;
67
+
68
+ typedef struct {
69
+ rgame_texture_sheet *sheet;
70
+ /* The part of the sheet this view samples, in sheet pixels. */
71
+ rgame_rect rect;
72
+ } rgame_texture;
73
+
74
+ /*
75
+ * Creates a sheet with one reference, or NULL if out of memory or given a
76
+ * degenerate size. `name` is whatever the GL layer allocated; this module
77
+ * treats it as an opaque token, so tests can pass any number they like.
78
+ */
79
+ rgame_texture_sheet *rgame_texture_sheet_create(unsigned int name, int width, int height);
80
+
81
+ void rgame_texture_sheet_retain(rgame_texture_sheet *sheet);
82
+
83
+ /*
84
+ * Drops one reference. Returns 1 if that was the last one — in which case the
85
+ * sheet has been freed and `*out_name` holds the GL name the caller must now
86
+ * delete — and 0 if others remain (`*out_name` untouched).
87
+ *
88
+ * Returning the name rather than deleting it is what keeps this file free of
89
+ * GL. `out_name` may not be NULL when the caller intends to honour the
90
+ * deletion; passing NULL is allowed and simply discards it, which is what the
91
+ * tests that only care about lifetimes do.
92
+ */
93
+ int rgame_texture_sheet_release(rgame_texture_sheet *sheet, unsigned int *out_name);
94
+
95
+ /*
96
+ * A view covering the whole sheet, retaining it. Every view — this one
97
+ * included — must eventually be passed to rgame_texture_destroy.
98
+ */
99
+ rgame_texture rgame_texture_whole(rgame_texture_sheet *sheet);
100
+
101
+ /* Drops this view's reference. Same return contract as sheet_release, so a
102
+ * caller can delete the GL texture when the last view goes. */
103
+ int rgame_texture_destroy(rgame_texture *view, unsigned int *out_name);
104
+
105
+ /* An independent view of the same region — retains the sheet. */
106
+ rgame_texture rgame_texture_clone(const rgame_texture *view);
107
+
108
+ /*
109
+ * A sub-rectangle of `view`, in coordinates *relative to that view* — so
110
+ * slicing a slice composes without the caller tracking absolute offsets.
111
+ *
112
+ * Returns 1 on success. Returns 0, leaving `*out` untouched, if the rect is
113
+ * degenerate or reaches outside the view: garbage UVs sample a neighbouring
114
+ * sprite, which looks like a drawing bug rather than the indexing bug it is,
115
+ * so this refuses instead of clamping.
116
+ */
117
+ int rgame_texture_subimage(const rgame_texture *view, int x, int y, int w, int h,
118
+ rgame_texture *out);
119
+
120
+ /* The GL texture name to bind when drawing this view, or 0 if it has no sheet
121
+ * — which the draw queue reads as "untextured". */
122
+ unsigned int rgame_texture_name(const rgame_texture *view);
123
+
124
+ int rgame_texture_width(const rgame_texture *view);
125
+ int rgame_texture_height(const rgame_texture *view);
126
+
127
+ /*
128
+ * How many whole `tile_width` x `tile_height` tiles fit, row-major. A partial
129
+ * tile at the right or bottom edge is not counted — a sheet whose size is not
130
+ * a multiple of the tile size has a strip of padding, and half a sprite is
131
+ * never what was meant. Zero or negative tile sizes yield 0.
132
+ */
133
+ int rgame_texture_tile_count(const rgame_texture *view, int tile_width, int tile_height);
134
+
135
+ /* The `index`-th tile, counting left to right then top to bottom. Returns 1 on
136
+ * success, 0 if the index is outside the count above. */
137
+ int rgame_texture_tile(const rgame_texture *view, int tile_width, int tile_height, int index,
138
+ rgame_texture *out);
139
+
140
+ /*
141
+ * The view's four corners in normalised texture coordinates, written to `uv8`
142
+ * as x,y pairs in the same corner order the canvas takes a quad's points:
143
+ * top-left, top-right, bottom-right, bottom-left.
144
+ *
145
+ * v runs downwards, matching the pixel rect and the screen: image row 0 is
146
+ * v = 0. That holds because image.c uploads the decoded rows in the order stb
147
+ * produces them (top row first), rather than flipping them the way a
148
+ * bottom-left-origin convention would need.
149
+ */
150
+ void rgame_texture_uv(const rgame_texture *view, float *uv8);
151
+
152
+ /*
153
+ * How many sheets are alive right now — for tests, not for gameplay.
154
+ *
155
+ * A leaked GPU texture is invisible: the game runs, the frame rate is fine,
156
+ * and video memory fills up over an hour of play. This counter makes "the
157
+ * texture went away when the last sprite using it did" an assertion, both in
158
+ * the Check suite and from Ruby, where the collector chooses the order in
159
+ * which an app and its images are swept.
160
+ *
161
+ * Single-threaded, like the rest of the engine.
162
+ */
163
+ long rgame_texture_live_sheets(void);
164
+
165
+ #endif /* RGAME_TEXTURE_H */
@@ -0,0 +1,128 @@
1
+ #include "graphics/transform.h"
2
+
3
+ #include <math.h>
4
+
5
+ /* Degrees in, radians out — the C library's trig wants radians, the API takes
6
+ * degrees because that is what callers already pass. */
7
+ #define RGAME_DEGREES_TO_RADIANS (3.14159265358979323846f / 180.0f)
8
+
9
+ /* Tolerance for "is this the identity". Composing and un-composing transforms
10
+ * accumulates float error, so an exact == would answer "no" for a stack that is
11
+ * identity in every way that matters. */
12
+ #define RGAME_IDENTITY_EPSILON 1e-6f
13
+
14
+ rgame_transform rgame_transform_identity(void) {
15
+ rgame_transform t = { .a = 1.0f, .b = 0.0f, .c = 0.0f, .d = 1.0f, .tx = 0.0f, .ty = 0.0f };
16
+ return t;
17
+ }
18
+
19
+ rgame_transform rgame_transform_multiply(rgame_transform outer, rgame_transform inner) {
20
+ /*
21
+ * Substituting inner's output into outer's formula and collecting terms:
22
+ *
23
+ * x' = outer.a*(inner.a*x + inner.c*y + inner.tx)
24
+ * + outer.c*(inner.b*x + inner.d*y + inner.ty) + outer.tx
25
+ *
26
+ * and likewise for y'. Written out rather than looped, because six named
27
+ * multiplies read better than a 2x3 loop and this is the hot path.
28
+ */
29
+ rgame_transform result;
30
+ result.a = (outer.a * inner.a) + (outer.c * inner.b);
31
+ result.b = (outer.b * inner.a) + (outer.d * inner.b);
32
+ result.c = (outer.a * inner.c) + (outer.c * inner.d);
33
+ result.d = (outer.b * inner.c) + (outer.d * inner.d);
34
+ result.tx = (outer.a * inner.tx) + (outer.c * inner.ty) + outer.tx;
35
+ result.ty = (outer.b * inner.tx) + (outer.d * inner.ty) + outer.ty;
36
+ return result;
37
+ }
38
+
39
+ void rgame_transform_stack_init(rgame_transform_stack *stack) {
40
+ stack->depth = 0;
41
+ stack->entries[0] = rgame_transform_identity();
42
+ }
43
+
44
+ /* Shared tail of the three pushes: compose with the current top, store, and
45
+ * report whether there was room. */
46
+ static int push_composed(rgame_transform_stack *stack, rgame_transform step) {
47
+ if (stack->depth + 1 >= RGAME_TRANSFORM_STACK_DEPTH) {
48
+ return 0;
49
+ }
50
+ stack->entries[stack->depth + 1] =
51
+ rgame_transform_multiply(stack->entries[stack->depth], step);
52
+ stack->depth++;
53
+ return 1;
54
+ }
55
+
56
+ int rgame_transform_push_translate(rgame_transform_stack *stack, float dx, float dy) {
57
+ rgame_transform step = rgame_transform_identity();
58
+ step.tx = dx;
59
+ step.ty = dy;
60
+ return push_composed(stack, step);
61
+ }
62
+
63
+ int rgame_transform_push_scale(rgame_transform_stack *stack, float sx, float sy) {
64
+ rgame_transform step = rgame_transform_identity();
65
+ step.a = sx;
66
+ step.d = sy;
67
+ return push_composed(stack, step);
68
+ }
69
+
70
+ int rgame_transform_push_rotate(rgame_transform_stack *stack, float degrees,
71
+ float pivot_x, float pivot_y) {
72
+ float radians = degrees * RGAME_DEGREES_TO_RADIANS;
73
+ float cosine = cosf(radians);
74
+ float sine = sinf(radians);
75
+
76
+ /*
77
+ * Rotating about a pivot is three steps: move the pivot to the origin,
78
+ * rotate, move it back. Composed here rather than pushed as three entries,
79
+ * so one push still matches one pop.
80
+ *
81
+ * The rotation itself is the textbook matrix. In screen space, where y
82
+ * points down, it turns *clockwise* for a positive angle — see the header.
83
+ */
84
+ rgame_transform rotation = { .a = cosine, .b = sine, .c = -sine, .d = cosine,
85
+ .tx = 0.0f, .ty = 0.0f };
86
+
87
+ rgame_transform to_origin = rgame_transform_identity();
88
+ to_origin.tx = -pivot_x;
89
+ to_origin.ty = -pivot_y;
90
+
91
+ rgame_transform back = rgame_transform_identity();
92
+ back.tx = pivot_x;
93
+ back.ty = pivot_y;
94
+
95
+ rgame_transform step =
96
+ rgame_transform_multiply(back, rgame_transform_multiply(rotation, to_origin));
97
+ return push_composed(stack, step);
98
+ }
99
+
100
+ void rgame_transform_pop(rgame_transform_stack *stack) {
101
+ if (stack->depth > 0) {
102
+ stack->depth--;
103
+ }
104
+ }
105
+
106
+ rgame_transform rgame_transform_current(const rgame_transform_stack *stack) {
107
+ return stack->entries[stack->depth];
108
+ }
109
+
110
+ int rgame_transform_depth(const rgame_transform_stack *stack) {
111
+ return stack->depth;
112
+ }
113
+
114
+ void rgame_transform_apply(const rgame_transform_stack *stack, float x, float y,
115
+ float *out_x, float *out_y) {
116
+ const rgame_transform *t = &stack->entries[stack->depth];
117
+ *out_x = (t->a * x) + (t->c * y) + t->tx;
118
+ *out_y = (t->b * x) + (t->d * y) + t->ty;
119
+ }
120
+
121
+ int rgame_transform_is_identity(const rgame_transform_stack *stack) {
122
+ const rgame_transform *t = &stack->entries[stack->depth];
123
+ return fabsf(t->a - 1.0f) < RGAME_IDENTITY_EPSILON &&
124
+ fabsf(t->b) < RGAME_IDENTITY_EPSILON &&
125
+ fabsf(t->c) < RGAME_IDENTITY_EPSILON &&
126
+ fabsf(t->d - 1.0f) < RGAME_IDENTITY_EPSILON &&
127
+ fabsf(t->tx) < RGAME_IDENTITY_EPSILON && fabsf(t->ty) < RGAME_IDENTITY_EPSILON;
128
+ }