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,216 @@
1
+ #include "graphics/draw_queue.h"
2
+
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ /*
7
+ * Grow a buffer to hold at least `needed` items, doubling so that repeated
8
+ * appends are amortised constant time. Returns 0 if the allocation failed, in
9
+ * which case the existing buffer is untouched and still usable — realloc only
10
+ * frees the old block on success.
11
+ */
12
+ static int ensure_capacity(void **items, unsigned int *capacity, unsigned int needed,
13
+ size_t item_size) {
14
+ if (*capacity >= needed) {
15
+ return 1;
16
+ }
17
+
18
+ unsigned int next = *capacity ? *capacity : 1;
19
+ while (next < needed) {
20
+ unsigned int doubled = next * 2;
21
+ if (doubled < next) { /* would overflow; give up rather than wrap */
22
+ return 0;
23
+ }
24
+ next = doubled;
25
+ }
26
+
27
+ void *grown = realloc(*items, (size_t)next * item_size);
28
+ if (!grown) {
29
+ return 0;
30
+ }
31
+ *items = grown;
32
+ *capacity = next;
33
+ return 1;
34
+ }
35
+
36
+ void rgame_draw_queue_init(rgame_draw_queue *queue) {
37
+ memset(queue, 0, sizeof(*queue));
38
+ }
39
+
40
+ void rgame_draw_queue_destroy(rgame_draw_queue *queue) {
41
+ free(queue->commands);
42
+ free(queue->vertices);
43
+ free(queue->sorted);
44
+ free(queue->batches);
45
+ free(queue->discard);
46
+ memset(queue, 0, sizeof(*queue));
47
+ }
48
+
49
+ void rgame_draw_queue_reset(rgame_draw_queue *queue) {
50
+ /* Counts only — the buffers stay, which is the whole point. */
51
+ queue->command_count = 0;
52
+ queue->vertex_count = 0;
53
+ queue->sorted_count = 0;
54
+ queue->batch_count = 0;
55
+ }
56
+
57
+ /* Somewhere writable for a dropped command's vertices to go. */
58
+ static rgame_vertex *discard_span(rgame_draw_queue *queue, unsigned int count) {
59
+ /* At least one, even for a zero-vertex request: the contract is that this
60
+ * never hands back NULL, and ensure_capacity(0) would short-circuit before
61
+ * allocating anything at all. */
62
+ unsigned int needed = count ? count : 1;
63
+ if (!ensure_capacity((void **)&queue->discard, &queue->discard_capacity, needed,
64
+ sizeof(rgame_vertex))) {
65
+ /*
66
+ * Even the scratch could not grow. Returning NULL here would hand the
67
+ * caller a pointer it does not check; instead keep whatever scratch we
68
+ * already have. Writing past it is only reachable if the very first
69
+ * allocation in the process failed, at which point nothing works
70
+ * anyway — but the pointer stays valid, which is what matters.
71
+ */
72
+ return queue->discard;
73
+ }
74
+ return queue->discard;
75
+ }
76
+
77
+ rgame_vertex *rgame_draw_queue_alloc(rgame_draw_queue *queue, unsigned int count, double z,
78
+ unsigned int texture, rgame_rect clip) {
79
+ /* Nothing to draw, or nowhere to draw it: drop before reserving anything. */
80
+ if (count == 0 || rgame_rect_is_empty(clip)) {
81
+ return discard_span(queue, count);
82
+ }
83
+
84
+ /*
85
+ * A NaN z makes the comparator intransitive, and qsort with a comparator
86
+ * that is not a total order is undefined behaviour rather than merely a
87
+ * wrong order. Concretely, with A(NaN, order 1), B(5.0, order 0) and
88
+ * C(1.0, order 2) the comparator reports B<A and A<C but also C<B.
89
+ * `z != z` is true only for NaN.
90
+ */
91
+ if (z != z) {
92
+ z = 0.0;
93
+ }
94
+
95
+ /* Reserve both buffers before committing to either, so a failure leaves
96
+ * the queue exactly as it was. */
97
+ if (!ensure_capacity((void **)&queue->vertices, &queue->vertex_capacity,
98
+ queue->vertex_count + count, sizeof(rgame_vertex)) ||
99
+ !ensure_capacity((void **)&queue->commands, &queue->command_capacity,
100
+ queue->command_count + 1, sizeof(rgame_draw_command))) {
101
+ return discard_span(queue, count);
102
+ }
103
+
104
+ rgame_draw_command *command = &queue->commands[queue->command_count];
105
+ command->z = z;
106
+ command->order = queue->command_count;
107
+ command->texture = texture;
108
+ command->clip = clip;
109
+ command->first_vertex = queue->vertex_count;
110
+ command->vertex_count = count;
111
+
112
+ rgame_vertex *span = &queue->vertices[queue->vertex_count];
113
+ queue->vertex_count += count;
114
+ queue->command_count++;
115
+ return span;
116
+ }
117
+
118
+ /*
119
+ * Order by z, then by insertion order.
120
+ *
121
+ * The tiebreak is what makes the result deterministic; because `order` is
122
+ * unique the comparison is a total order, so an unstable sort like qsort still
123
+ * produces the one correct answer. Stability comes from the comparator, not
124
+ * from the algorithm.
125
+ */
126
+ int rgame_draw_command_compare(const void *lhs, const void *rhs) {
127
+ const rgame_draw_command *a = lhs;
128
+ const rgame_draw_command *b = rhs;
129
+
130
+ if (a->z < b->z) {
131
+ return -1;
132
+ }
133
+ if (a->z > b->z) {
134
+ return 1;
135
+ }
136
+ if (a->order < b->order) {
137
+ return -1;
138
+ }
139
+ return a->order > b->order ? 1 : 0;
140
+ }
141
+
142
+ /* Two commands can share a draw call only if both the texture and the clip
143
+ * match — see the note on `clip` in the header for why the clip is part of it. */
144
+ static int batchable_with(const rgame_draw_batch *batch, const rgame_draw_command *command) {
145
+ return batch->texture == command->texture && rgame_rect_equals(batch->clip, command->clip);
146
+ }
147
+
148
+ void rgame_draw_queue_prepare(rgame_draw_queue *queue) {
149
+ queue->sorted_count = 0;
150
+ queue->batch_count = 0;
151
+ if (queue->command_count == 0) {
152
+ return;
153
+ }
154
+
155
+ qsort(queue->commands, queue->command_count, sizeof(rgame_draw_command),
156
+ rgame_draw_command_compare);
157
+
158
+ if (!ensure_capacity((void **)&queue->sorted, &queue->sorted_capacity, queue->vertex_count,
159
+ sizeof(rgame_vertex)) ||
160
+ !ensure_capacity((void **)&queue->batches, &queue->batch_capacity,
161
+ queue->command_count, sizeof(rgame_draw_batch))) {
162
+ return; /* nothing prepared; the frame draws nothing rather than crashing */
163
+ }
164
+
165
+ for (unsigned int i = 0; i < queue->command_count; i++) {
166
+ const rgame_draw_command *command = &queue->commands[i];
167
+
168
+ /* Copy this command's vertices into the contiguous output. */
169
+ memcpy(&queue->sorted[queue->sorted_count], &queue->vertices[command->first_vertex],
170
+ (size_t)command->vertex_count * sizeof(rgame_vertex));
171
+
172
+ rgame_draw_batch *open_batch =
173
+ queue->batch_count ? &queue->batches[queue->batch_count - 1] : NULL;
174
+
175
+ if (open_batch && batchable_with(open_batch, command)) {
176
+ open_batch->vertex_count += command->vertex_count;
177
+ } else {
178
+ rgame_draw_batch *batch = &queue->batches[queue->batch_count++];
179
+ batch->texture = command->texture;
180
+ batch->clip = command->clip;
181
+ batch->first_vertex = queue->sorted_count;
182
+ batch->vertex_count = command->vertex_count;
183
+ }
184
+
185
+ queue->sorted_count += command->vertex_count;
186
+ }
187
+ }
188
+
189
+ unsigned int rgame_draw_queue_batch_count(const rgame_draw_queue *queue) {
190
+ return queue->batch_count;
191
+ }
192
+
193
+ const rgame_draw_batch *rgame_draw_queue_batch(const rgame_draw_queue *queue,
194
+ unsigned int index) {
195
+ return index < queue->batch_count ? &queue->batches[index] : NULL;
196
+ }
197
+
198
+ const rgame_vertex *rgame_draw_queue_vertices(const rgame_draw_queue *queue) {
199
+ return queue->sorted;
200
+ }
201
+
202
+ unsigned int rgame_draw_queue_vertex_count(const rgame_draw_queue *queue) {
203
+ return queue->sorted_count;
204
+ }
205
+
206
+ unsigned int rgame_draw_queue_command_count(const rgame_draw_queue *queue) {
207
+ return queue->command_count;
208
+ }
209
+
210
+ unsigned int rgame_draw_queue_command_capacity(const rgame_draw_queue *queue) {
211
+ return queue->command_capacity;
212
+ }
213
+
214
+ unsigned int rgame_draw_queue_vertex_capacity(const rgame_draw_queue *queue) {
215
+ return queue->vertex_capacity;
216
+ }
@@ -0,0 +1,174 @@
1
+ #ifndef RGAME_DRAW_QUEUE_H
2
+ #define RGAME_DRAW_QUEUE_H
3
+
4
+ #include "graphics/clip.h"
5
+
6
+ /*
7
+ * The draw queue: z-ordering and batching — pure arithmetic, no SDL, no GL.
8
+ *
9
+ * ---------------------------------------------------------------------------
10
+ * Why this exists at all
11
+ * ---------------------------------------------------------------------------
12
+ *
13
+ * Every draw call carries a `z`, and the renderer must sort by it *regardless
14
+ * of the order the calls were made in*. UI draws above gameplay, overlays above
15
+ * UI, all by z value rather than by anyone remembering to draw in the right
16
+ * sequence. The layer being replaced depends on this everywhere.
17
+ *
18
+ * The obvious shortcut — enable the depth buffer and let the GPU sort it out —
19
+ * does not work here, and it is worth being clear why: depth testing and alpha
20
+ * blending are mutually exclusive. A depth-tested translucent quad *rejects*
21
+ * the fragments behind it instead of blending with them, so anything drawn
22
+ * later at a lower z simply vanishes. Every UI panel in this engine is
23
+ * translucent chrome over gameplay, so the depth buffer would break exactly the
24
+ * case it was supposed to help. Hence: collect commands, sort them on the CPU,
25
+ * and draw back-to-front with blending on.
26
+ *
27
+ * ---------------------------------------------------------------------------
28
+ * How it is arranged
29
+ * ---------------------------------------------------------------------------
30
+ *
31
+ * Vertices go in one big array; commands index into it. That keeps the records
32
+ * being sorted small (a command is ~40 bytes, not six vertices), and lets
33
+ * triangles and quads sit in the same queue without a variant type.
34
+ *
35
+ * `prepare` then does two things: sorts the commands, and walks them building a
36
+ * *second*, sorted vertex array plus a list of batches. The copy is necessary —
37
+ * after sorting, a batch's commands are adjacent but their vertices are still
38
+ * scattered through the original array, and a GL draw call needs one contiguous
39
+ * run.
40
+ *
41
+ * Nothing here talks to a backend. `prepare` leaves batches and vertices for
42
+ * someone else to walk, which keeps the dependency one-directional and means
43
+ * the whole module is testable with no fake and no display.
44
+ */
45
+
46
+ /*
47
+ * One vertex, 20 bytes: position, texture coordinate, colour.
48
+ *
49
+ * The colour is four *bytes* in R, G, B, A order rather than a packed 32-bit
50
+ * word, and that is deliberate. glColorPointer(4, GL_UNSIGNED_BYTE, ...) reads
51
+ * them in memory order, and the bytes of a packed 0xRRGGBBAA come out A, B, G,
52
+ * R on a little-endian machine — GL would read alpha as red. Storing bytes
53
+ * sidesteps the question and is endian-independent. See rgame_color_bytes.
54
+ */
55
+ typedef struct {
56
+ float x, y; /* screen space: already transformed when it arrives here */
57
+ float u, v; /* texture coordinates; ignored when texture == 0 */
58
+ unsigned char rgba[4];
59
+ } rgame_vertex;
60
+
61
+ typedef struct {
62
+ double z;
63
+ /*
64
+ * Insertion index. The sort compares (z, order), so commands with equal z
65
+ * keep the order they were issued in — without
66
+ * it same-z sprites would swap places between frames as the sort reshuffled
67
+ * them, which reads as flicker. Note the *comparator* provides stability
68
+ * here, so the sort algorithm itself does not have to be stable.
69
+ */
70
+ unsigned int order;
71
+ unsigned int texture; /* GL texture name; 0 means untextured */
72
+ /*
73
+ * The clip travels with the command rather than being ambient state,
74
+ * because sorting reorders commands across viewports. A clip left "current"
75
+ * at draw time would end up scissoring whichever quads happened to sort
76
+ * next to it — the bug a single-viewport renderer cannot see and cannot fix
77
+ * without being rebuilt.
78
+ */
79
+ rgame_rect clip;
80
+ unsigned int first_vertex, vertex_count;
81
+ } rgame_draw_command;
82
+
83
+ /* A run of adjacent sorted commands that share a texture and a clip, and can
84
+ * therefore go to the GPU as one call. */
85
+ typedef struct {
86
+ unsigned int texture;
87
+ rgame_rect clip;
88
+ unsigned int first_vertex, vertex_count; /* into the prepared vertex array */
89
+ } rgame_draw_batch;
90
+
91
+ typedef struct {
92
+ rgame_draw_command *commands;
93
+ unsigned int command_count, command_capacity;
94
+
95
+ rgame_vertex *vertices; /* as submitted */
96
+ unsigned int vertex_count, vertex_capacity;
97
+
98
+ rgame_vertex *sorted; /* rebuilt by prepare, contiguous per batch */
99
+ unsigned int sorted_count, sorted_capacity;
100
+
101
+ rgame_draw_batch *batches;
102
+ unsigned int batch_count, batch_capacity;
103
+
104
+ /*
105
+ * Where vertices go when a command is dropped (empty clip, or a failed
106
+ * grow). The caller still gets somewhere writable, so it never has to
107
+ * null-check and can never write through a null pointer; the vertices are
108
+ * simply never referenced. Bounded by the largest single request.
109
+ */
110
+ rgame_vertex *discard;
111
+ unsigned int discard_capacity;
112
+ } rgame_draw_queue;
113
+
114
+ void rgame_draw_queue_init(rgame_draw_queue *queue);
115
+
116
+ /* Releases every buffer. The queue is reusable after a fresh init. */
117
+ void rgame_draw_queue_destroy(rgame_draw_queue *queue);
118
+
119
+ /*
120
+ * Empties the queue for a new frame while *keeping* the buffers it has grown.
121
+ * After a few frames the capacities settle and a steady frame allocates
122
+ * nothing at all, which is the difference between a smooth 60fps and a GC-style
123
+ * hitch every time the heap grows.
124
+ */
125
+ void rgame_draw_queue_reset(rgame_draw_queue *queue);
126
+
127
+ /*
128
+ * Reserves `count` vertices for one primitive and returns a writable span for
129
+ * the caller to fill in place — no temporary array, no copy.
130
+ *
131
+ * Never returns NULL. A command with an empty clip, a zero vertex count, or one
132
+ * that could not be stored is silently dropped, and the returned span points at
133
+ * scratch. That is deliberate: a caller that forgets to check a return value
134
+ * should get a draw that does not appear, not a crash.
135
+ */
136
+ rgame_vertex *rgame_draw_queue_alloc(rgame_draw_queue *queue, unsigned int count, double z,
137
+ unsigned int texture, rgame_rect clip);
138
+
139
+ /*
140
+ * Sorts the commands and builds the batch list and the contiguous vertex array
141
+ * that goes with it. Call once per frame, after all drawing and before handing
142
+ * the result to a backend.
143
+ */
144
+ void rgame_draw_queue_prepare(rgame_draw_queue *queue);
145
+
146
+ /* Valid after prepare. */
147
+ unsigned int rgame_draw_queue_batch_count(const rgame_draw_queue *queue);
148
+ const rgame_draw_batch *rgame_draw_queue_batch(const rgame_draw_queue *queue,
149
+ unsigned int index);
150
+ const rgame_vertex *rgame_draw_queue_vertices(const rgame_draw_queue *queue);
151
+ unsigned int rgame_draw_queue_vertex_count(const rgame_draw_queue *queue);
152
+
153
+ /*
154
+ * The ordering `prepare` sorts by: ascending z, ties broken by insertion order.
155
+ *
156
+ * Exposed rather than kept static because it cannot be tested through the
157
+ * sort: the C library's qsort is free to be stable, and glibc's is, so a
158
+ * missing tiebreak would still *look* correct here while being undefined
159
+ * anywhere else. Signature matches qsort's comparator.
160
+ */
161
+ int rgame_draw_command_compare(const void *lhs, const void *rhs);
162
+
163
+ /* How many commands are waiting. Mostly for tests and diagnostics. */
164
+ unsigned int rgame_draw_queue_command_count(const rgame_draw_queue *queue);
165
+
166
+ /*
167
+ * Current buffer capacities. Exposed so a test can assert that a second
168
+ * identical frame grows nothing — the "no per-frame allocation" property is
169
+ * invisible otherwise, and would rot without something watching it.
170
+ */
171
+ unsigned int rgame_draw_queue_command_capacity(const rgame_draw_queue *queue);
172
+ unsigned int rgame_draw_queue_vertex_capacity(const rgame_draw_queue *queue);
173
+
174
+ #endif /* RGAME_DRAW_QUEUE_H */
@@ -0,0 +1,122 @@
1
+ /*
2
+ * gl_backend.c — the real GL calls. See gl_backend.h for what this is and
3
+ * which OpenGL it targets.
4
+ */
5
+
6
+ #include "graphics/gl_backend.h"
7
+
8
+ #include <SDL2/SDL_opengl.h>
9
+
10
+ /* The colour an un-drawn pixel ends up. Not configurable yet — when a game
11
+ * needs its own background it should become an app-level setting rather than a
12
+ * constant here. */
13
+ #define RGAME_CLEAR_R 0.1f
14
+ #define RGAME_CLEAR_G 0.1f
15
+ #define RGAME_CLEAR_B 0.15f
16
+
17
+ static void gl_begin_frame(void *ctx, int width, int height) {
18
+ rgame_gl_backend *state = ctx;
19
+ state->width = width;
20
+ state->height = height;
21
+
22
+ glViewport(0, 0, width, height);
23
+
24
+ /*
25
+ * Screen coordinates, not GL's. glOrtho's near/far arguments are given as
26
+ * (left, right, bottom, top): passing height as *bottom* and 0 as *top*
27
+ * flips the y axis, so (0,0) is the top-left corner and y grows downwards —
28
+ * the convention every coordinate in this engine already uses.
29
+ */
30
+ glMatrixMode(GL_PROJECTION);
31
+ glLoadIdentity();
32
+ glOrtho(0.0, (double)width, (double)height, 0.0, -1.0, 1.0);
33
+ glMatrixMode(GL_MODELVIEW);
34
+ glLoadIdentity();
35
+
36
+ /*
37
+ * No depth testing. The draw queue has already sorted by z on the CPU, and
38
+ * that is not an optimisation to undo: depth testing and alpha blending do
39
+ * not mix. A translucent pixel that passes the depth test writes depth, so
40
+ * whatever should have shown through it is discarded — which is why
41
+ * translucent UI over gameplay needs a painter's-algorithm sort instead.
42
+ */
43
+ glDisable(GL_DEPTH_TEST);
44
+
45
+ glEnable(GL_BLEND);
46
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
47
+
48
+ /* Clear before the scissor test goes on: glClear obeys the scissor, so
49
+ * clearing afterwards would only clear whatever region was last set. */
50
+ glDisable(GL_SCISSOR_TEST);
51
+ glClearColor(RGAME_CLEAR_R, RGAME_CLEAR_G, RGAME_CLEAR_B, 1.0f);
52
+ glClear(GL_COLOR_BUFFER_BIT);
53
+ glEnable(GL_SCISSOR_TEST);
54
+
55
+ /* Client-side vertex arrays: the vertex data stays in our own buffer and GL
56
+ * reads it at draw time. Switching these on once per frame rather than once
57
+ * per batch is the whole reason they are here and not in draw_batch. */
58
+ glEnableClientState(GL_VERTEX_ARRAY);
59
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
60
+ glEnableClientState(GL_COLOR_ARRAY);
61
+ }
62
+
63
+ static void gl_set_clip(void *ctx, rgame_rect clip) {
64
+ rgame_gl_backend *state = ctx;
65
+
66
+ /*
67
+ * GL measures the scissor box from the *bottom* of the window; every
68
+ * rectangle in this engine is measured from the top. Flipping it is one
69
+ * line and getting it wrong puts the clip in the mirror-image place, which
70
+ * on a centred test rectangle looks correct.
71
+ */
72
+ glScissor(clip.x, state->height - (clip.y + clip.h), clip.w, clip.h);
73
+ }
74
+
75
+ static void gl_draw_batch(void *ctx, unsigned int texture, const rgame_vertex *vertices,
76
+ unsigned int count) {
77
+ (void)ctx;
78
+
79
+ if (texture == 0) {
80
+ /* Untextured: the colour array alone decides the pixel. */
81
+ glDisable(GL_TEXTURE_2D);
82
+ } else {
83
+ glEnable(GL_TEXTURE_2D);
84
+ glBindTexture(GL_TEXTURE_2D, texture);
85
+ }
86
+
87
+ /*
88
+ * The three arrays are interleaved in one `rgame_vertex`, so all three
89
+ * pointers walk the same buffer with the same stride at different offsets.
90
+ * One allocation per frame, one pass over it per batch.
91
+ */
92
+ glVertexPointer(2, GL_FLOAT, sizeof(rgame_vertex), &vertices[0].x);
93
+ glTexCoordPointer(2, GL_FLOAT, sizeof(rgame_vertex), &vertices[0].u);
94
+ glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(rgame_vertex), vertices[0].rgba);
95
+
96
+ /* Everything is triangles by the time it reaches here — quads were split
97
+ * upstream, so there is one primitive type to issue. */
98
+ glDrawArrays(GL_TRIANGLES, 0, (GLsizei)count);
99
+ }
100
+
101
+ static void gl_end_frame(void *ctx) {
102
+ (void)ctx;
103
+
104
+ /* Leave GL as it was found. The window's own buffer swap is the app's job,
105
+ * not the backend's — the backend has no window. */
106
+ glDisableClientState(GL_VERTEX_ARRAY);
107
+ glDisableClientState(GL_TEXTURE_COORD_ARRAY);
108
+ glDisableClientState(GL_COLOR_ARRAY);
109
+ glDisable(GL_SCISSOR_TEST);
110
+ glDisable(GL_TEXTURE_2D);
111
+ }
112
+
113
+ rgame_draw_backend rgame_gl_backend_table(rgame_gl_backend *state) {
114
+ rgame_draw_backend backend = {
115
+ .begin_frame = gl_begin_frame,
116
+ .set_clip = gl_set_clip,
117
+ .draw_batch = gl_draw_batch,
118
+ .end_frame = gl_end_frame,
119
+ .ctx = state,
120
+ };
121
+ return backend;
122
+ }
@@ -0,0 +1,43 @@
1
+ #ifndef RGAME_GL_BACKEND_H
2
+ #define RGAME_GL_BACKEND_H
3
+
4
+ #include "graphics/backend.h"
5
+
6
+ /*
7
+ * The real OpenGL implementation of the `rgame_draw_backend` table — layer 3,
8
+ * and the only file in the drawing path that calls `gl*`.
9
+ *
10
+ * Everything above it has already decided what to draw and in what order:
11
+ * primitives.c turned shapes into quads, canvas.c transformed and clipped them,
12
+ * draw_queue.c sorted and batched them. What is left here is issuing the calls,
13
+ * which is why the file is short and why it is verified by looking at the
14
+ * screen (`make run`, `rake spec:core`'s pixel checks) rather than by unit
15
+ * tests — the same calls in the same order are already checked against
16
+ * `test/support/recording_backend.c`.
17
+ *
18
+ * ---------------------------------------------------------------------------
19
+ * Which OpenGL this is
20
+ * ---------------------------------------------------------------------------
21
+ *
22
+ * Fixed-function, compatibility-profile GL 1.1: `glOrtho`, client-side vertex
23
+ * arrays, `glDrawArrays`. No shaders and no extension loader (GLAD/GLEW),
24
+ * because none of it needs one — every function used here is in the 1.1 core
25
+ * that ships with the platform's GL library. Moving to core-profile modern GL
26
+ * would be a deliberate decision with a loader attached; see CLAUDE.md.
27
+ */
28
+
29
+ typedef struct {
30
+ /* Remembered from begin_frame so set_clip can flip a screen-space rectangle
31
+ * into GL's bottom-up scissor coordinates. */
32
+ int width, height;
33
+ } rgame_gl_backend;
34
+
35
+ /*
36
+ * A backend table driving real GL, over caller-owned state.
37
+ *
38
+ * `state` must outlive the returned table — it is the `ctx` every callback
39
+ * receives. Typically both live in the app.
40
+ */
41
+ rgame_draw_backend rgame_gl_backend_table(rgame_gl_backend *state);
42
+
43
+ #endif /* RGAME_GL_BACKEND_H */