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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- 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 */
|