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,52 @@
1
+ #include "graphics/backend.h"
2
+
3
+ void rgame_draw_submit(const rgame_draw_queue *queue, const rgame_draw_backend *backend,
4
+ int width, int height) {
5
+ if (!queue || !backend) {
6
+ return;
7
+ }
8
+
9
+ if (backend->begin_frame) {
10
+ backend->begin_frame(backend->ctx, width, height);
11
+ }
12
+
13
+ /*
14
+ * Track the clip actually in force rather than assuming each batch needs a
15
+ * fresh one. `have_clip` distinguishes "no clip set yet" from "the current
16
+ * clip happens to equal the zero rect", which a plain comparison against a
17
+ * zeroed variable could not.
18
+ *
19
+ * No batch can currently carry the zero rect — an empty clip makes the
20
+ * queue drop the command before it ever becomes a batch — so the flag is
21
+ * unreachable today, and a test cannot distinguish it. It is kept because
22
+ * the invariant that saves it lives in *another* module: if the queue ever
23
+ * stopped dropping empty-clipped commands, the first batch of a frame would
24
+ * silently go unscissored.
25
+ */
26
+ rgame_rect current_clip = { 0, 0, 0, 0 };
27
+ int have_clip = 0;
28
+
29
+ const rgame_vertex *vertices = rgame_draw_queue_vertices(queue);
30
+ unsigned int batches = rgame_draw_queue_batch_count(queue);
31
+
32
+ for (unsigned int i = 0; i < batches; i++) {
33
+ const rgame_draw_batch *batch = rgame_draw_queue_batch(queue, i);
34
+
35
+ if (!have_clip || !rgame_rect_equals(current_clip, batch->clip)) {
36
+ if (backend->set_clip) {
37
+ backend->set_clip(backend->ctx, batch->clip);
38
+ }
39
+ current_clip = batch->clip;
40
+ have_clip = 1;
41
+ }
42
+
43
+ if (backend->draw_batch) {
44
+ backend->draw_batch(backend->ctx, batch->texture, &vertices[batch->first_vertex],
45
+ batch->vertex_count);
46
+ }
47
+ }
48
+
49
+ if (backend->end_frame) {
50
+ backend->end_frame(backend->ctx);
51
+ }
52
+ }
@@ -0,0 +1,64 @@
1
+ #ifndef RGAME_BACKEND_H
2
+ #define RGAME_BACKEND_H
3
+
4
+ #include "graphics/clip.h"
5
+ #include "graphics/draw_queue.h"
6
+
7
+ /*
8
+ * The drawing backend seam — "layer 2" in CLAUDE.md's abstraction strategy.
9
+ *
10
+ * Everything up to here is arithmetic: the canvas positions vertices, the queue
11
+ * sorts and batches them. This is where that stops and real GL calls begin, so
12
+ * this is where the function-pointer table goes — added at the point real calls
13
+ * appear rather than speculatively ahead of it.
14
+ *
15
+ * Two implementations exist by design: the real one that talks to OpenGL, and a
16
+ * recording one the Check suite substitutes to assert that the right primitive
17
+ * calls happened in the right order, with no display anywhere in sight.
18
+ *
19
+ * ---------------------------------------------------------------------------
20
+ * What the seam is *not* for
21
+ * ---------------------------------------------------------------------------
22
+ *
23
+ * It is not how the queue and canvas get tested. Those hand back their prepared
24
+ * batches directly and are exercised without any backend at all, which keeps
25
+ * the dependency one-directional: draw_queue and canvas do not include this
26
+ * header. What is genuinely worth a fake is the submit loop below — the small
27
+ * amount of logic that sits between a prepared frame and the GPU.
28
+ */
29
+
30
+ /*
31
+ * Every member may be NULL, in which case that call is skipped. `ctx` is the
32
+ * implementation's own state, passed back to each function.
33
+ *
34
+ * Fixed arity throughout, like the app callbacks: a variadic convention on a
35
+ * per-frame path marshals arguments for no benefit.
36
+ */
37
+ typedef struct {
38
+ void (*begin_frame)(void *ctx, int width, int height);
39
+ void (*set_clip)(void *ctx, rgame_rect clip);
40
+ void (*draw_batch)(void *ctx, unsigned int texture, const rgame_vertex *vertices,
41
+ unsigned int count);
42
+ void (*end_frame)(void *ctx);
43
+ void *ctx;
44
+ } rgame_draw_backend;
45
+
46
+ /*
47
+ * Walks a *prepared* queue and drives the backend:
48
+ *
49
+ * begin_frame
50
+ * set_clip / draw_batch ... (per batch, clip only when it changes)
51
+ * end_frame
52
+ *
53
+ * `set_clip` is issued only when the rectangle actually differs from the last
54
+ * one set. Batches split on either texture or clip, so two adjacent batches
55
+ * often share a clip and differ only in texture — re-issuing an identical
56
+ * scissor for each would be a wasted state change every frame.
57
+ *
58
+ * begin_frame and end_frame are called even for an empty queue: the real
59
+ * backend still has to clear and present the frame.
60
+ */
61
+ void rgame_draw_submit(const rgame_draw_queue *queue, const rgame_draw_backend *backend,
62
+ int width, int height);
63
+
64
+ #endif /* RGAME_BACKEND_H */
@@ -0,0 +1,247 @@
1
+ #include "graphics/canvas.h"
2
+
3
+ #include <math.h>
4
+
5
+ /* What a recorded push has to undo. */
6
+ enum {
7
+ RGAME_PUSH_TRANSFORM,
8
+ RGAME_PUSH_CLIP,
9
+ /* The underlying stack was full. Nothing to undo, but it still occupies a
10
+ * slot so that the caller's matching pop stays paired with this push. */
11
+ RGAME_PUSH_NOTHING
12
+ };
13
+
14
+ void rgame_canvas_init(rgame_canvas *canvas) {
15
+ rgame_draw_queue_init(&canvas->queue);
16
+ rgame_transform_stack_init(&canvas->transforms);
17
+ rgame_clip_stack_init(&canvas->clips, 0, 0);
18
+ canvas->push_depth = 0;
19
+ canvas->unrecorded_pushes = 0;
20
+ canvas->width = 0;
21
+ canvas->height = 0;
22
+ }
23
+
24
+ void rgame_canvas_destroy(rgame_canvas *canvas) {
25
+ rgame_draw_queue_destroy(&canvas->queue);
26
+ }
27
+
28
+ void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height) {
29
+ rgame_draw_queue_reset(&canvas->queue);
30
+ rgame_transform_stack_init(&canvas->transforms);
31
+ rgame_clip_stack_init(&canvas->clips, width, height);
32
+ canvas->push_depth = 0;
33
+ canvas->unrecorded_pushes = 0;
34
+ canvas->width = width;
35
+ canvas->height = height;
36
+ }
37
+
38
+ void rgame_canvas_end_frame(rgame_canvas *canvas) {
39
+ rgame_draw_queue_prepare(&canvas->queue);
40
+ }
41
+
42
+ /* Records what a successful push has to undo. Returns 0 when there is no room
43
+ * to record it, in which case the caller counts it as unrecorded instead. */
44
+ static int record_push(rgame_canvas *canvas, unsigned char kind) {
45
+ if (canvas->push_depth >= RGAME_CANVAS_STACK_DEPTH) {
46
+ return 0;
47
+ }
48
+ canvas->pushes[canvas->push_depth++] = kind;
49
+ return 1;
50
+ }
51
+
52
+ /*
53
+ * Shared tail of every push: if the underlying stack accepted it, remember
54
+ * which one to undo; otherwise remember that there is nothing to undo. Either
55
+ * way a push is accounted for, so pop always has something to consume.
56
+ */
57
+ static void account_push(rgame_canvas *canvas, int accepted, unsigned char kind) {
58
+ if (!record_push(canvas, accepted ? kind : RGAME_PUSH_NOTHING)) {
59
+ canvas->unrecorded_pushes++;
60
+ }
61
+ }
62
+
63
+ void rgame_canvas_push_translate(rgame_canvas *canvas, float dx, float dy) {
64
+ int ok = rgame_transform_push_translate(&canvas->transforms, dx, dy);
65
+ account_push(canvas, ok, RGAME_PUSH_TRANSFORM);
66
+ }
67
+
68
+ void rgame_canvas_push_rotate(rgame_canvas *canvas, float degrees, float pivot_x,
69
+ float pivot_y) {
70
+ int ok = rgame_transform_push_rotate(&canvas->transforms, degrees, pivot_x, pivot_y);
71
+ account_push(canvas, ok, RGAME_PUSH_TRANSFORM);
72
+ }
73
+
74
+ void rgame_canvas_push_scale(rgame_canvas *canvas, float sx, float sy) {
75
+ int ok = rgame_transform_push_scale(&canvas->transforms, sx, sy);
76
+ account_push(canvas, ok, RGAME_PUSH_TRANSFORM);
77
+ }
78
+
79
+ /*
80
+ * Maps a rect through the current transform and takes the axis-aligned bounding
81
+ * box of the result.
82
+ *
83
+ * All four corners are mapped, not just two: under rotation the box is decided
84
+ * by whichever corners happen to be extreme, and under a mirroring scale the
85
+ * "top-left" corner is no longer top-left. Bounds are rounded outwards so a
86
+ * partially covered pixel is kept rather than clipped away.
87
+ */
88
+ static rgame_rect transformed_bounds(const rgame_canvas *canvas, rgame_rect rect) {
89
+ float corners[4][2] = { { (float)rect.x, (float)rect.y },
90
+ { (float)(rect.x + rect.w), (float)rect.y },
91
+ { (float)(rect.x + rect.w), (float)(rect.y + rect.h) },
92
+ { (float)rect.x, (float)(rect.y + rect.h) } };
93
+
94
+ float min_x = 0.0f, min_y = 0.0f, max_x = 0.0f, max_y = 0.0f;
95
+ for (int i = 0; i < 4; i++) {
96
+ float x, y;
97
+ rgame_transform_apply(&canvas->transforms, corners[i][0], corners[i][1], &x, &y);
98
+ if (i == 0) {
99
+ min_x = max_x = x;
100
+ min_y = max_y = y;
101
+ continue;
102
+ }
103
+ if (x < min_x) {
104
+ min_x = x;
105
+ }
106
+ if (x > max_x) {
107
+ max_x = x;
108
+ }
109
+ if (y < min_y) {
110
+ min_y = y;
111
+ }
112
+ if (y > max_y) {
113
+ max_y = y;
114
+ }
115
+ }
116
+
117
+ int left = (int)floorf(min_x);
118
+ int top = (int)floorf(min_y);
119
+ int right = (int)ceilf(max_x);
120
+ int bottom = (int)ceilf(max_y);
121
+ return rgame_rect_make(left, top, right - left, bottom - top);
122
+ }
123
+
124
+ void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect) {
125
+ int ok = rgame_clip_push(&canvas->clips, transformed_bounds(canvas, rect));
126
+ account_push(canvas, ok, RGAME_PUSH_CLIP);
127
+ }
128
+
129
+ void rgame_canvas_pop(rgame_canvas *canvas) {
130
+ /* Unrecorded pushes unwind first: they are the most recent ones, since a
131
+ * push is only left unrecorded once the record stack is already full. */
132
+ if (canvas->unrecorded_pushes > 0) {
133
+ canvas->unrecorded_pushes--;
134
+ return;
135
+ }
136
+ if (canvas->push_depth == 0) {
137
+ return; /* unbalanced pop; harmless */
138
+ }
139
+
140
+ switch (canvas->pushes[--canvas->push_depth]) {
141
+ case RGAME_PUSH_TRANSFORM:
142
+ rgame_transform_pop(&canvas->transforms);
143
+ break;
144
+ case RGAME_PUSH_CLIP:
145
+ rgame_clip_pop(&canvas->clips);
146
+ break;
147
+ default:
148
+ break; /* RGAME_PUSH_NOTHING: the push never took effect */
149
+ }
150
+ }
151
+
152
+ /* Fills one vertex: map the point into screen space, copy through the texture
153
+ * coordinate, and write the colour as the four bytes GL reads. */
154
+ static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float x, float y,
155
+ float u, float v, rgame_color color) {
156
+ rgame_transform_apply(&canvas->transforms, x, y, &vertex->x, &vertex->y);
157
+ vertex->u = u;
158
+ vertex->v = v;
159
+ rgame_color_bytes(color, vertex->rgba);
160
+ }
161
+
162
+ void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color color,
163
+ double z) {
164
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, z, 0,
165
+ rgame_clip_current(&canvas->clips));
166
+ for (int i = 0; i < 3; i++) {
167
+ write_vertex(canvas, &out[i], xy6[i * 2], xy6[(i * 2) + 1], 0.0f, 0.0f, color);
168
+ }
169
+ }
170
+
171
+ /* Corner indices for the two triangles a quad becomes. */
172
+ static const int RGAME_QUAD_TRIANGLES[6] = { 0, 1, 2, 0, 2, 3 };
173
+
174
+ void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color, double z) {
175
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, 0,
176
+ rgame_clip_current(&canvas->clips));
177
+ for (int i = 0; i < 6; i++) {
178
+ int corner = RGAME_QUAD_TRIANGLES[i];
179
+ write_vertex(canvas, &out[i], xy8[corner * 2], xy8[(corner * 2) + 1], 0.0f, 0.0f,
180
+ color);
181
+ }
182
+ }
183
+
184
+ void rgame_canvas_textured_quad(rgame_canvas *canvas, unsigned int texture, const float *xy8,
185
+ const float *uv8, rgame_color color, double z) {
186
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, texture,
187
+ rgame_clip_current(&canvas->clips));
188
+ for (int i = 0; i < 6; i++) {
189
+ int corner = RGAME_QUAD_TRIANGLES[i];
190
+ write_vertex(canvas, &out[i], xy8[corner * 2], xy8[(corner * 2) + 1], uv8[corner * 2],
191
+ uv8[(corner * 2) + 1], color);
192
+ }
193
+ }
194
+
195
+ /* Multiplies two colour components, 0..255 in and out: 255 leaves the other
196
+ * untouched, which is what makes WHITE the "no tint" value. */
197
+ static unsigned char modulate(unsigned char value, int tint) {
198
+ return (unsigned char)((value * tint) / 255);
199
+ }
200
+
201
+ void rgame_canvas_replay(rgame_canvas *canvas, const rgame_recording *recording, float dx,
202
+ float dy, rgame_color color, double z) {
203
+ if (!recording) {
204
+ return;
205
+ }
206
+
207
+ int tint[4] = { rgame_color_r(color), rgame_color_g(color), rgame_color_b(color),
208
+ rgame_color_a(color) };
209
+
210
+ for (unsigned int b = 0; b < recording->batch_count; b++) {
211
+ const rgame_recording_batch *batch = &recording->batches[b];
212
+
213
+ /* One command per baked batch, rather than one per original draw call:
214
+ * the whole point of a recording is that the per-tile work happened
215
+ * once, at bake time. */
216
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, z,
217
+ batch->texture,
218
+ rgame_clip_current(&canvas->clips));
219
+
220
+ for (unsigned int i = 0; i < batch->vertex_count; i++) {
221
+ const rgame_vertex *baked = &recording->vertices[batch->first_vertex + i];
222
+
223
+ /* Offset first, then the current transform — so the offset is in
224
+ * the recording's own coordinates and the transform is the world's,
225
+ * which is the order a camera needs. */
226
+ rgame_transform_apply(&canvas->transforms, baked->x + dx, baked->y + dy, &out[i].x,
227
+ &out[i].y);
228
+ out[i].u = baked->u;
229
+ out[i].v = baked->v;
230
+ for (int c = 0; c < 4; c++) {
231
+ out[i].rgba[c] = modulate(baked->rgba[c], tint[c]);
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ void rgame_canvas_submit(const rgame_canvas *canvas, const rgame_draw_backend *backend) {
238
+ rgame_draw_submit(&canvas->queue, backend, canvas->width, canvas->height);
239
+ }
240
+
241
+ const rgame_draw_queue *rgame_canvas_queue(const rgame_canvas *canvas) {
242
+ return &canvas->queue;
243
+ }
244
+
245
+ int rgame_canvas_depth(const rgame_canvas *canvas) {
246
+ return canvas->push_depth + canvas->unrecorded_pushes;
247
+ }
@@ -0,0 +1,143 @@
1
+ #ifndef RGAME_CANVAS_H
2
+ #define RGAME_CANVAS_H
3
+
4
+ #include "graphics/backend.h"
5
+ #include "graphics/clip.h"
6
+ #include "color.h"
7
+ #include "graphics/draw_queue.h"
8
+ #include "graphics/recording.h"
9
+ #include "graphics/transform.h"
10
+
11
+ /*
12
+ * The canvas: where the transform stack, the clip stack and the draw queue meet
13
+ * — and still pure arithmetic, with no SDL and no GL.
14
+ *
15
+ * Each of the three pieces below it does one thing and knows nothing of the
16
+ * others. This is the layer that composes them, and it is the seam the drawing
17
+ * API is actually written against: everything above (the Ruby renderer, and
18
+ * eventually the scene graph) speaks to a canvas, and everything below is
19
+ * arithmetic that can be tested without a window.
20
+ *
21
+ * ---------------------------------------------------------------------------
22
+ * Vertices are transformed on the way in
23
+ * ---------------------------------------------------------------------------
24
+ *
25
+ * A primitive's points are mapped through the current transform *before* they
26
+ * reach the queue, so the queue stores screen-space coordinates. That is what
27
+ * lets it sort freely: once a vertex is positioned, reordering commands cannot
28
+ * change where anything lands. Storing untransformed points and applying the
29
+ * transform at flush time would mean re-associating each command with the
30
+ * transform that was current when it was issued — which is exactly the
31
+ * bookkeeping this design exists to avoid.
32
+ *
33
+ * ---------------------------------------------------------------------------
34
+ * Clips are transformed too
35
+ * ---------------------------------------------------------------------------
36
+ *
37
+ * A clip pushed inside a translate moves with it — measured against the layer
38
+ * being replaced, where a clip at x 0..20 inside a translate of 50 clips
39
+ * x 50..70. So `push_clip` maps the caller's rect through the current transform
40
+ * before handing it to the clip stack, which works purely in screen space.
41
+ *
42
+ * Under translation and scaling that mapping is exact. Under rotation it cannot
43
+ * be: a scissor rectangle is axis-aligned and a rotated rectangle is not, so
44
+ * the axis-aligned bounding box is used instead. That errs towards clipping
45
+ * *less* than asked, which is the safe direction, and nothing in the engine
46
+ * rotates a clip today.
47
+ *
48
+ * ---------------------------------------------------------------------------
49
+ * One pop for any push
50
+ * ---------------------------------------------------------------------------
51
+ *
52
+ * `push_translate`, `push_rotate`, `push_scale` and `push_clip` are all undone
53
+ * by the same `pop`. The canvas remembers which stack each push went to, so a
54
+ * caller never has to — and cannot pop the wrong one. Pushes that could not be
55
+ * honoured (a full stack) are still counted, so pop stays balanced and the
56
+ * drawing comes out untransformed rather than desynchronised.
57
+ */
58
+
59
+ /* Deep enough that it cannot fill before both underlying stacks have; pushes
60
+ * beyond that are counted rather than recorded, so balance always holds. */
61
+ #define RGAME_CANVAS_STACK_DEPTH (RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH)
62
+
63
+ typedef struct {
64
+ rgame_transform_stack transforms;
65
+ rgame_clip_stack clips;
66
+ rgame_draw_queue queue;
67
+
68
+ /* Which stack each outstanding push went to, so pop can undo the right one. */
69
+ unsigned char pushes[RGAME_CANVAS_STACK_DEPTH];
70
+ int push_depth;
71
+ /* Pushes with nowhere to be recorded. Counted so pop still balances. */
72
+ int unrecorded_pushes;
73
+
74
+ /* Remembered from begin_frame, so submit needs no repeat of the size. */
75
+ int width, height;
76
+ } rgame_canvas;
77
+
78
+ void rgame_canvas_init(rgame_canvas *canvas);
79
+ void rgame_canvas_destroy(rgame_canvas *canvas);
80
+
81
+ /*
82
+ * Starts a frame: empties the queue (keeping its buffers), resets both stacks,
83
+ * and sets the clip base to the window. Re-stating the size every frame is also
84
+ * how a resize takes effect, so there is no separate path to forget.
85
+ */
86
+ void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height);
87
+
88
+ /* Sorts and batches what was drawn. Read the result through rgame_canvas_queue. */
89
+ void rgame_canvas_end_frame(rgame_canvas *canvas);
90
+
91
+ void rgame_canvas_push_translate(rgame_canvas *canvas, float dx, float dy);
92
+ void rgame_canvas_push_rotate(rgame_canvas *canvas, float degrees, float pivot_x,
93
+ float pivot_y);
94
+ void rgame_canvas_push_scale(rgame_canvas *canvas, float sx, float sy);
95
+
96
+ /* `rect` is in the caller's local coordinates and is mapped through the current
97
+ * transform before narrowing the clip. */
98
+ void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect);
99
+
100
+ void rgame_canvas_pop(rgame_canvas *canvas);
101
+
102
+ /*
103
+ * Primitives. Points arrive as flat coordinate pairs in the caller's local
104
+ * space: `xy6` is three points, `xy8` four. A quad's points are taken in loop
105
+ * order (for a rectangle: top-left, top-right, bottom-right, bottom-left) and
106
+ * split into triangles 0-1-2 and 0-2-3.
107
+ *
108
+ * `uv8` is the matching four texture coordinates, in the same corner order.
109
+ */
110
+ void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color color,
111
+ double z);
112
+ void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color, double z);
113
+ void rgame_canvas_textured_quad(rgame_canvas *canvas, unsigned int texture, const float *xy8,
114
+ const float *uv8, rgame_color color, double z);
115
+
116
+ /*
117
+ * Replays a baked recording (see recording.h) offset by (dx, dy), at `z`, tinted
118
+ * by `color` — RGAME_COLOR_WHITE leaves the recorded colours alone.
119
+ *
120
+ * The offset is applied *before* the current transform, so a baked layer moves
121
+ * with the camera it is drawn under and can be placed anywhere without being
122
+ * re-baked. Every batch becomes one command at the same z, and because equal z
123
+ * keeps insertion order, the painter order baked into the recording survives.
124
+ *
125
+ * The clip is the one current at replay time; recordings do not carry their own
126
+ * (recording.h says why).
127
+ */
128
+ void rgame_canvas_replay(rgame_canvas *canvas, const rgame_recording *recording, float dx,
129
+ float dy, rgame_color color, double z);
130
+
131
+ /*
132
+ * Hands the finished frame to a backend. Call after end_frame; equivalent to
133
+ * rgame_draw_submit with the size this frame was begun at.
134
+ */
135
+ void rgame_canvas_submit(const rgame_canvas *canvas, const rgame_draw_backend *backend);
136
+
137
+ const rgame_draw_queue *rgame_canvas_queue(const rgame_canvas *canvas);
138
+
139
+ /* How many pushes are outstanding, counting ones that could not be recorded.
140
+ * A balanced frame ends at zero. */
141
+ int rgame_canvas_depth(const rgame_canvas *canvas);
142
+
143
+ #endif /* RGAME_CANVAS_H */
@@ -0,0 +1,87 @@
1
+ #include "graphics/clip.h"
2
+
3
+ /* The canonical empty rect. Every path that produces "nothing" produces this
4
+ * exact value, so two empties compare equal and cannot split a batch. */
5
+ static const rgame_rect RGAME_EMPTY_RECT = { 0, 0, 0, 0 };
6
+
7
+ rgame_rect rgame_rect_make(int x, int y, int w, int h) {
8
+ if (w <= 0 || h <= 0) {
9
+ return RGAME_EMPTY_RECT;
10
+ }
11
+ rgame_rect r = { x, y, w, h };
12
+ return r;
13
+ }
14
+
15
+ int rgame_rect_is_empty(rgame_rect r) {
16
+ return r.w <= 0 || r.h <= 0;
17
+ }
18
+
19
+ int rgame_rect_equals(rgame_rect a, rgame_rect b) {
20
+ return a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h;
21
+ }
22
+
23
+ int rgame_rect_contains_point(rgame_rect r, int x, int y) {
24
+ return !rgame_rect_is_empty(r) && x >= r.x && y >= r.y && x < r.x + r.w && y < r.y + r.h;
25
+ }
26
+
27
+ rgame_rect rgame_rect_intersect(rgame_rect a, rgame_rect b) {
28
+ if (rgame_rect_is_empty(a) || rgame_rect_is_empty(b)) {
29
+ return RGAME_EMPTY_RECT;
30
+ }
31
+
32
+ /*
33
+ * Edges are computed in `long` rather than `int`. A caller can legitimately
34
+ * pass a very large width to mean "as wide as possible", and x + w would
35
+ * then overflow — which is undefined behaviour, not merely a wrong answer.
36
+ * The result always fits back in an int because it is bounded by the
37
+ * smaller of the two inputs.
38
+ */
39
+ long left = a.x > b.x ? a.x : b.x;
40
+ long top = a.y > b.y ? a.y : b.y;
41
+ long a_right = (long)a.x + a.w;
42
+ long b_right = (long)b.x + b.w;
43
+ long a_bottom = (long)a.y + a.h;
44
+ long b_bottom = (long)b.y + b.h;
45
+ long right = a_right < b_right ? a_right : b_right;
46
+ long bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
47
+
48
+ if (right <= left || bottom <= top) {
49
+ return RGAME_EMPTY_RECT;
50
+ }
51
+
52
+ rgame_rect result = { (int)left, (int)top, (int)(right - left), (int)(bottom - top) };
53
+ return result;
54
+ }
55
+
56
+ void rgame_clip_stack_init(rgame_clip_stack *stack, int width, int height) {
57
+ stack->depth = 0;
58
+ stack->entries[0] = rgame_rect_make(0, 0, width, height);
59
+ }
60
+
61
+ int rgame_clip_push(rgame_clip_stack *stack, rgame_rect rect) {
62
+ if (stack->depth + 1 >= RGAME_CLIP_STACK_DEPTH) {
63
+ return 0;
64
+ }
65
+ stack->entries[stack->depth + 1] =
66
+ rgame_rect_intersect(stack->entries[stack->depth], rect);
67
+ stack->depth++;
68
+ return 1;
69
+ }
70
+
71
+ void rgame_clip_pop(rgame_clip_stack *stack) {
72
+ if (stack->depth > 0) {
73
+ stack->depth--;
74
+ }
75
+ }
76
+
77
+ rgame_rect rgame_clip_current(const rgame_clip_stack *stack) {
78
+ return stack->entries[stack->depth];
79
+ }
80
+
81
+ int rgame_clip_is_empty(const rgame_clip_stack *stack) {
82
+ return rgame_rect_is_empty(stack->entries[stack->depth]);
83
+ }
84
+
85
+ int rgame_clip_depth(const rgame_clip_stack *stack) {
86
+ return stack->depth;
87
+ }
@@ -0,0 +1,89 @@
1
+ #ifndef RGAME_CLIP_H
2
+ #define RGAME_CLIP_H
3
+
4
+ /*
5
+ * Rectangles and the clip stack — pure arithmetic, no SDL, no GL, no I/O.
6
+ *
7
+ * Clipping restricts drawing to a rectangle. The engine needs it in two very
8
+ * different places: tiling a 9-slice texture's edges without overdraw, and
9
+ * giving each player their own screen region in split-screen. Both want the
10
+ * same thing — a stack where pushing *narrows* the visible area and popping
11
+ * restores it.
12
+ *
13
+ * ---------------------------------------------------------------------------
14
+ * Screen space, and whose job the transform is
15
+ * ---------------------------------------------------------------------------
16
+ *
17
+ * Everything here is in screen pixels. That is deliberate, and it is *not* the
18
+ * whole story for callers: in the layer being replaced, a clip pushed inside a
19
+ * translate moves with it (measured — a clip at x 0..20 inside translate(50,0)
20
+ * clips x 50..70). Reproducing that is the canvas's job: it maps the caller's
21
+ * rect through the current transform and pushes the screen-space result here.
22
+ *
23
+ * Keeping the two apart is what lets this module stay pure integer arithmetic
24
+ * with no idea that transforms exist.
25
+ *
26
+ * ---------------------------------------------------------------------------
27
+ * Empty is a real answer
28
+ * ---------------------------------------------------------------------------
29
+ *
30
+ * Two disjoint rects intersect to nothing, and "nothing" has to be
31
+ * representable — the draw queue uses it to drop commands before they ever
32
+ * reach the GPU. An empty result is canonicalised to {0,0,0,0} so that two
33
+ * different ways of ending up with nothing compare equal, which matters
34
+ * because the clip rect is part of the queue's batch key.
35
+ */
36
+
37
+ typedef struct {
38
+ int x, y, w, h;
39
+ } rgame_rect;
40
+
41
+ /* Deep enough for any sane scene; a bounded stack cannot run away. */
42
+ #define RGAME_CLIP_STACK_DEPTH 32
43
+
44
+ typedef struct {
45
+ /* entries[0] is the window bounds — the widest anything may draw — so
46
+ * every command has a meaningful clip and there is no "no clip" case. */
47
+ rgame_rect entries[RGAME_CLIP_STACK_DEPTH];
48
+ int depth;
49
+ } rgame_clip_stack;
50
+
51
+ rgame_rect rgame_rect_make(int x, int y, int w, int h);
52
+
53
+ /* The overlap of two rects, or {0,0,0,0} if they do not overlap. */
54
+ rgame_rect rgame_rect_intersect(rgame_rect a, rgame_rect b);
55
+
56
+ int rgame_rect_is_empty(rgame_rect r);
57
+
58
+ /* Exact equality. The draw queue compares clips to decide whether two commands
59
+ * can share a batch, so this needs to be cheap and total. */
60
+ int rgame_rect_equals(rgame_rect a, rgame_rect b);
61
+
62
+ int rgame_rect_contains_point(rgame_rect r, int x, int y);
63
+
64
+ /*
65
+ * Resets the stack with the window bounds as its base. Called at the start of
66
+ * every frame, which is also how a window resize takes effect — there is no
67
+ * separate "the window changed size" path to forget.
68
+ */
69
+ void rgame_clip_stack_init(rgame_clip_stack *stack, int width, int height);
70
+
71
+ /*
72
+ * Narrows the clip to `rect` intersected with the current one, and pushes the
73
+ * result. Returns 1, or 0 if the stack is full — in which case nothing is
74
+ * pushed and no matching pop must be issued.
75
+ *
76
+ * Note this always *narrows*: a pushed rect larger than the current clip
77
+ * cannot widen it. That is what makes nesting safe — a child can never draw
78
+ * outside the region its parent allowed.
79
+ */
80
+ int rgame_clip_push(rgame_clip_stack *stack, rgame_rect rect);
81
+
82
+ /* Pops the current clip. Popping the base is a no-op rather than an underflow. */
83
+ void rgame_clip_pop(rgame_clip_stack *stack);
84
+
85
+ rgame_rect rgame_clip_current(const rgame_clip_stack *stack);
86
+ int rgame_clip_is_empty(const rgame_clip_stack *stack);
87
+ int rgame_clip_depth(const rgame_clip_stack *stack);
88
+
89
+ #endif /* RGAME_CLIP_H */