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,385 @@
1
+ /*
2
+ * font_atlas.c — the one file in the text stack that touches GL.
3
+ *
4
+ * Everything interesting about text is elsewhere and pure: font.c knows what
5
+ * glyphs measure and look like, atlas.c knows where the next one goes on a
6
+ * page, glyph_cache.c knows which have been done already. This composes the
7
+ * three, owns the GL textures behind the pages, and reads the font file. That
8
+ * is layer 3 in CLAUDE.md's abstraction strategy, and it is kept this thin
9
+ * precisely so "we don't unit-test it directly" is honest rather than a gap —
10
+ * `spec_core/rgame/core/font_spec.rb` and the renderer's pixel checks exercise
11
+ * it end to end against a real GL context.
12
+ *
13
+ * ---------------------------------------------------------------------------
14
+ * Why the pages are GL_ALPHA
15
+ * ---------------------------------------------------------------------------
16
+ *
17
+ * A rasterised glyph is coverage: one byte per pixel saying how much ink is
18
+ * there. Uploading that as RGBA would cost four times the memory to say the
19
+ * same thing three redundant ways.
20
+ *
21
+ * An alpha texture under the fixed-function default (GL_MODULATE) gives
22
+ * `rgb = the vertex colour, a = vertex alpha * coverage`, which is exactly what
23
+ * coloured text is. No shader, no second code path in the backend — the glyph
24
+ * quads go through the same textured-quad call sprites do.
25
+ *
26
+ * Worth knowing for the day the project moves to core-profile GL: GL_ALPHA does
27
+ * not exist there. The equivalent is GL_RED plus a swizzle in a shader.
28
+ */
29
+
30
+ #include "rgame/core.h"
31
+
32
+ #include "app/app_gl.h"
33
+ #include "text/atlas.h"
34
+ #include "text/font.h"
35
+ #include "text/font_internal.h"
36
+ #include "text/glyph_cache.h"
37
+
38
+ #include <SDL2/SDL_opengl.h>
39
+ #include <stdio.h>
40
+ #include <stdlib.h>
41
+
42
+ /*
43
+ * Big enough that a Latin character set fits on one page — a few hundred glyphs
44
+ * at the sizes a game uses — and small enough that a font nobody draws much
45
+ * with has not cost a megabyte. 256 KB per page, one byte per pixel.
46
+ *
47
+ * A page that fills is not a problem: another one opens, and the only cost is
48
+ * that a string spanning both takes two draw calls instead of one.
49
+ */
50
+ #define RGAME_FONT_PAGE_SIZE 512
51
+
52
+ typedef struct {
53
+ unsigned int texture; /* GL name */
54
+ rgame_atlas atlas;
55
+ } rgame_font_page;
56
+
57
+ struct rgame_font {
58
+ rgame_app *app; /* retained, like an image's — see image.c */
59
+ rgame_typeface *typeface;
60
+ rgame_glyph_cache cache;
61
+
62
+ rgame_font_page *pages;
63
+ int page_count, page_capacity;
64
+
65
+ /* Reused for every rasterisation rather than malloc'd per glyph. Grown to
66
+ * the largest glyph seen and never shrunk; a font's biggest glyph is small
67
+ * and is met within the first few words. */
68
+ unsigned char *scratch;
69
+ size_t scratch_size;
70
+ };
71
+
72
+ /* See rgame_font_live_pages. Single-threaded, like the rest of the engine. */
73
+ static long live_pages = 0;
74
+
75
+ long rgame_font_live_pages(void) {
76
+ return live_pages;
77
+ }
78
+
79
+ /* ------------------------------------------------------------------------- *
80
+ * Pages
81
+ * ------------------------------------------------------------------------- */
82
+
83
+ /*
84
+ * Allocates a blank page on the GPU. The caller must already have made the
85
+ * font's GL context current.
86
+ *
87
+ * The page is uploaded zeroed rather than left undefined: the gutters between
88
+ * glyphs are sampled by linear filtering at every glyph's edge, and undefined
89
+ * memory there is a fringe of noise around every letter.
90
+ */
91
+ static int add_page(rgame_font *font) {
92
+ if (font->page_count == font->page_capacity) {
93
+ int capacity = font->page_capacity ? font->page_capacity * 2 : 2;
94
+ rgame_font_page *pages = realloc(font->pages, sizeof(rgame_font_page) * (size_t)capacity);
95
+ if (!pages) {
96
+ return 0;
97
+ }
98
+ font->pages = pages;
99
+ font->page_capacity = capacity;
100
+ }
101
+
102
+ unsigned char *blank = calloc(RGAME_FONT_PAGE_SIZE * RGAME_FONT_PAGE_SIZE, 1);
103
+ if (!blank) {
104
+ return 0;
105
+ }
106
+
107
+ unsigned int texture = 0;
108
+ glGenTextures(1, &texture);
109
+ if (texture == 0) {
110
+ free(blank);
111
+ return 0;
112
+ }
113
+
114
+ glBindTexture(GL_TEXTURE_2D, texture);
115
+ /* One byte per pixel and an arbitrary glyph width, so GL's default
116
+ * four-byte row alignment would shear every upload. */
117
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
118
+ /* Linear, unlike sprites: text is not pixel art, and stb hands us
119
+ * antialiased coverage that nearest sampling would throw away. */
120
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
121
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
122
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
123
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
124
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, RGAME_FONT_PAGE_SIZE, RGAME_FONT_PAGE_SIZE, 0,
125
+ GL_ALPHA, GL_UNSIGNED_BYTE, blank);
126
+ glBindTexture(GL_TEXTURE_2D, 0);
127
+
128
+ free(blank);
129
+
130
+ rgame_font_page *page = &font->pages[font->page_count++];
131
+ page->texture = texture;
132
+ rgame_atlas_init(&page->atlas, RGAME_FONT_PAGE_SIZE, RGAME_FONT_PAGE_SIZE);
133
+ live_pages++;
134
+ return 1;
135
+ }
136
+
137
+ /* ------------------------------------------------------------------------- *
138
+ * Loading
139
+ * ------------------------------------------------------------------------- */
140
+
141
+ static void set_error(char *err, size_t err_size, const char *format, const char *detail) {
142
+ if (err && err_size > 0) {
143
+ snprintf(err, err_size, format, detail);
144
+ }
145
+ }
146
+
147
+ /* Same shape as image.c's reader: the engine decides how a missing file is
148
+ * reported, not the library that parses what is in it. */
149
+ static unsigned char *read_whole_file(const char *path, long *out_size) {
150
+ FILE *file = fopen(path, "rb");
151
+ if (!file) {
152
+ return NULL;
153
+ }
154
+
155
+ if (fseek(file, 0, SEEK_END) != 0) {
156
+ fclose(file);
157
+ return NULL;
158
+ }
159
+ long size = ftell(file);
160
+ if (size <= 0) {
161
+ fclose(file);
162
+ return NULL;
163
+ }
164
+ rewind(file);
165
+
166
+ unsigned char *bytes = malloc((size_t)size);
167
+ if (!bytes) {
168
+ fclose(file);
169
+ return NULL;
170
+ }
171
+
172
+ size_t got = fread(bytes, 1, (size_t)size, file);
173
+ fclose(file);
174
+ if (got != (size_t)size) {
175
+ free(bytes);
176
+ return NULL;
177
+ }
178
+
179
+ *out_size = size;
180
+ return bytes;
181
+ }
182
+
183
+ rgame_font *rgame_font_load(rgame_app *app, const char *path, int pixel_height, char *err,
184
+ size_t err_size) {
185
+ if (!app || !path) {
186
+ set_error(err, err_size, "%s", "no app or path given");
187
+ return NULL;
188
+ }
189
+ if (pixel_height <= 0) {
190
+ set_error(err, err_size, "%s", "a font size must be positive");
191
+ return NULL;
192
+ }
193
+
194
+ long size = 0;
195
+ unsigned char *bytes = read_whole_file(path, &size);
196
+ if (!bytes) {
197
+ set_error(err, err_size, "could not read %s", path);
198
+ return NULL;
199
+ }
200
+
201
+ rgame_font *font = calloc(1, sizeof(rgame_font));
202
+ if (!font) {
203
+ free(bytes);
204
+ set_error(err, err_size, "%s", "out of memory");
205
+ return NULL;
206
+ }
207
+
208
+ /* The face copies what it needs, so the file buffer goes straight back. */
209
+ font->typeface = rgame_typeface_open(bytes, (size_t)size, pixel_height);
210
+ free(bytes);
211
+ if (!font->typeface) {
212
+ free(font);
213
+ set_error(err, err_size, "could not read %s as a TrueType font", path);
214
+ return NULL;
215
+ }
216
+
217
+ rgame_glyph_cache_init(&font->cache);
218
+ rgame_app_gl_retain(app);
219
+ font->app = app;
220
+
221
+ /* No page yet. A font that is only ever measured — a layout pass that never
222
+ * draws — costs no video memory at all. */
223
+ return font;
224
+ }
225
+
226
+ void rgame_font_destroy(rgame_font *font) {
227
+ if (!font) {
228
+ return;
229
+ }
230
+
231
+ if (font->page_count > 0) {
232
+ /* Deleting a texture acts on whatever context is current, so say which
233
+ * and put back what was there — a collector picks this moment, quite
234
+ * possibly in the middle of another window's frame. If the app is
235
+ * already gone the pages went with its context, and there is nothing
236
+ * to delete. */
237
+ rgame_gl_context_save saved;
238
+ if (rgame_app_gl_make_current(font->app, &saved)) {
239
+ for (int i = 0; i < font->page_count; i++) {
240
+ glDeleteTextures(1, &font->pages[i].texture);
241
+ }
242
+ }
243
+ rgame_app_gl_restore(&saved);
244
+ live_pages -= font->page_count;
245
+ }
246
+
247
+ free(font->pages);
248
+ free(font->scratch);
249
+ rgame_glyph_cache_destroy(&font->cache);
250
+ rgame_typeface_close(font->typeface);
251
+ rgame_app_gl_release(font->app);
252
+ free(font);
253
+ }
254
+
255
+ /* ------------------------------------------------------------------------- *
256
+ * Queries
257
+ * ------------------------------------------------------------------------- */
258
+
259
+ int rgame_font_height(const rgame_font *font) {
260
+ return font ? rgame_typeface_height(font->typeface) : 0;
261
+ }
262
+
263
+ float rgame_font_measure(const rgame_font *font, const char *text, size_t length) {
264
+ /* Measuring touches no GL and needs no page, so it works outside a frame —
265
+ * which matters, because laying out a menu happens in update, not draw. */
266
+ return font ? rgame_typeface_measure(font->typeface, text, length) : 0.0f;
267
+ }
268
+
269
+ const rgame_typeface *rgame_font_typeface(const rgame_font *font) {
270
+ return font ? font->typeface : NULL;
271
+ }
272
+
273
+ rgame_app *rgame_font_owner(const rgame_font *font) {
274
+ return font ? font->app : NULL;
275
+ }
276
+
277
+ /* ------------------------------------------------------------------------- *
278
+ * Rasterising on demand
279
+ * ------------------------------------------------------------------------- */
280
+
281
+ /* Packs a glyph onto a page with room for it, opening a new one if the last is
282
+ * full. Returns the page index, or -1. */
283
+ static int place_on_a_page(rgame_font *font, int width, int height, rgame_rect *out) {
284
+ if (font->page_count > 0 &&
285
+ rgame_atlas_place(&font->pages[font->page_count - 1].atlas, width, height, out)) {
286
+ return font->page_count - 1;
287
+ }
288
+
289
+ if (!add_page(font)) {
290
+ return -1;
291
+ }
292
+ if (!rgame_atlas_place(&font->pages[font->page_count - 1].atlas, width, height, out)) {
293
+ /* Bigger than a whole page. Nothing to be done for it, and a fresh page
294
+ * would refuse it too — so give up on this glyph rather than opening a
295
+ * page per attempt for the rest of the run. */
296
+ return -1;
297
+ }
298
+ return font->page_count - 1;
299
+ }
300
+
301
+ static int ensure_scratch(rgame_font *font, size_t needed) {
302
+ if (font->scratch_size >= needed) {
303
+ return 1;
304
+ }
305
+
306
+ unsigned char *scratch = realloc(font->scratch, needed);
307
+ if (!scratch) {
308
+ return 0;
309
+ }
310
+ font->scratch = scratch;
311
+ font->scratch_size = needed;
312
+ return 1;
313
+ }
314
+
315
+ int rgame_font_glyph(rgame_font *font, int codepoint, rgame_glyph *out, unsigned int *texture,
316
+ int *page_width, int *page_height) {
317
+ if (!font || !out) {
318
+ return 0;
319
+ }
320
+
321
+ if (page_width) {
322
+ *page_width = RGAME_FONT_PAGE_SIZE;
323
+ }
324
+ if (page_height) {
325
+ *page_height = RGAME_FONT_PAGE_SIZE;
326
+ }
327
+
328
+ if (rgame_glyph_cache_find(&font->cache, codepoint, out)) {
329
+ if (texture) {
330
+ *texture = font->pages[out->page].texture;
331
+ }
332
+ return 1;
333
+ }
334
+
335
+ rgame_glyph glyph;
336
+ if (!rgame_typeface_glyph(font->typeface, codepoint, &glyph)) {
337
+ return 0;
338
+ }
339
+
340
+ /*
341
+ * A glyph with no ink — a space — is cached with an empty rectangle and
342
+ * never touches a page or the GPU. Its advance is the whole reason it is
343
+ * cached at all.
344
+ */
345
+ if (glyph.rect.w > 0 && glyph.rect.h > 0) {
346
+ rgame_gl_context_save saved;
347
+ if (!rgame_app_gl_make_current(font->app, &saved)) {
348
+ rgame_app_gl_restore(&saved);
349
+ return 0;
350
+ }
351
+
352
+ rgame_rect placed;
353
+ int page = place_on_a_page(font, glyph.rect.w, glyph.rect.h, &placed);
354
+ size_t pixels = (size_t)glyph.rect.w * (size_t)glyph.rect.h;
355
+
356
+ if (page < 0 || !ensure_scratch(font, pixels)) {
357
+ rgame_app_gl_restore(&saved);
358
+ return 0;
359
+ }
360
+
361
+ rgame_typeface_render(font->typeface, codepoint, font->scratch, glyph.rect.w,
362
+ glyph.rect.w, glyph.rect.h);
363
+
364
+ glBindTexture(GL_TEXTURE_2D, font->pages[page].texture);
365
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
366
+ glTexSubImage2D(GL_TEXTURE_2D, 0, placed.x, placed.y, placed.w, placed.h, GL_ALPHA,
367
+ GL_UNSIGNED_BYTE, font->scratch);
368
+ glBindTexture(GL_TEXTURE_2D, 0);
369
+
370
+ rgame_app_gl_restore(&saved);
371
+
372
+ glyph.page = page;
373
+ glyph.rect = placed;
374
+ }
375
+
376
+ /* A cache that could not grow is not fatal: the glyph is drawn from what
377
+ * was just rasterised and simply gets rasterised again next time. */
378
+ rgame_glyph_cache_insert(&font->cache, &glyph);
379
+
380
+ *out = glyph;
381
+ if (texture) {
382
+ *texture = glyph.rect.w > 0 ? font->pages[glyph.page].texture : 0;
383
+ }
384
+ return 1;
385
+ }
@@ -0,0 +1,47 @@
1
+ #ifndef RGAME_FONT_INTERNAL_H
2
+ #define RGAME_FONT_INTERNAL_H
3
+
4
+ #include "text/font.h"
5
+ #include "rgame/core.h"
6
+
7
+ /*
8
+ * What the drawing path needs from inside an `rgame_font`.
9
+ *
10
+ * `rgame_font` is opaque in the public header and stays that way — a caller
11
+ * outside this extension has no business knowing what an atlas page is. But
12
+ * app.c has to walk a string and place its glyphs, so the accessors live here,
13
+ * in a header that is not under include/ and therefore cannot leave
14
+ * ext/rgame_core/. Same arrangement as image_internal.h.
15
+ */
16
+
17
+ /* The face behind the font, for measuring and walking a string. */
18
+ const rgame_typeface *rgame_font_typeface(const rgame_font *font);
19
+
20
+ /* The app whose GL context the atlas pages live in, or NULL. Compared against
21
+ * the app being drawn into, for the same reason images are — a texture from
22
+ * another context samples nothing and paints white. */
23
+ rgame_app *rgame_font_owner(const rgame_font *font);
24
+
25
+ /*
26
+ * A glyph ready to draw: cached if it has been seen, and otherwise rasterised,
27
+ * packed onto a page and uploaded on the spot.
28
+ *
29
+ * Also reports which GL texture to bind and how big it is, since the caller
30
+ * needs both to turn the glyph's page rectangle into texture coordinates.
31
+ *
32
+ * Returns 1 on success, 0 if the glyph could not be made — out of memory, or an
33
+ * atlas page that refused a glyph bigger than a whole page. A caller should
34
+ * skip that glyph and carry on drawing the rest of the string.
35
+ *
36
+ * Uploading mid-frame is fine: uploads happen immediately while drawing is
37
+ * deferred, so the page is complete long before the frame is submitted.
38
+ */
39
+ int rgame_font_glyph(rgame_font *font, int codepoint, rgame_glyph *out, unsigned int *texture,
40
+ int *page_width, int *page_height);
41
+
42
+ /* How many atlas pages exist across all live fonts — for tests, the way
43
+ * rgame_texture_live_sheets is for images. A leaked page is invisible
44
+ * otherwise. */
45
+ long rgame_font_live_pages(void);
46
+
47
+ #endif /* RGAME_FONT_INTERNAL_H */
@@ -0,0 +1,142 @@
1
+ /*
2
+ * glyph_cache.c — an open-addressed table of rasterised glyphs.
3
+ * See glyph_cache.h for why it caches per glyph and why nothing is ever evicted.
4
+ */
5
+
6
+ #include "text/glyph_cache.h"
7
+
8
+ #include <stdlib.h>
9
+
10
+ /* Small enough that a font drawing only digits wastes nothing, big enough that
11
+ * plain ASCII text never rehashes. */
12
+ #define RGAME_GLYPH_CACHE_MIN_CAPACITY 64
13
+
14
+ /* Grow at 3/4 full. Linear probing degrades sharply as a table approaches
15
+ * full — the probe walk turns into a scan — so the table is kept loose. */
16
+ #define RGAME_GLYPH_CACHE_LOAD_NUMERATOR 3
17
+ #define RGAME_GLYPH_CACHE_LOAD_DENOMINATOR 4
18
+
19
+ void rgame_glyph_cache_init(rgame_glyph_cache *cache) {
20
+ cache->entries = NULL;
21
+ cache->capacity = 0;
22
+ cache->count = 0;
23
+ }
24
+
25
+ void rgame_glyph_cache_destroy(rgame_glyph_cache *cache) {
26
+ if (!cache) {
27
+ return;
28
+ }
29
+
30
+ free(cache->entries);
31
+ /* Left in the state init would produce, so a second destroy is a no-op and
32
+ * a find afterwards is an ordinary miss rather than a use-after-free. */
33
+ rgame_glyph_cache_init(cache);
34
+ }
35
+
36
+ /*
37
+ * Knuth's multiplicative hash, masked to the table size.
38
+ *
39
+ * Note what this is *not*: a correctness requirement. Replacing it with the
40
+ * identity function leaves every test passing, and rightly so — linear probing
41
+ * resolves collisions whatever the hash does, so a worse one is slower, not
42
+ * wrong. That mutation survives the suite deliberately.
43
+ *
44
+ * It stays because the cost is one multiply and the alternative is a bet on the
45
+ * key distribution. Identity happens to spread the codepoints a Latin game
46
+ * draws almost perfectly, since they arrive in contiguous runs (ASCII 32..126,
47
+ * Latin-1 accents, a handful of punctuation). The bet is on the next game:
48
+ * masking the low bits of a table this small means an emoji range, or CJK, or
49
+ * any set whose members share their low bits, lands in a few buckets and turns
50
+ * every lookup into a walk. Not worth finding out.
51
+ */
52
+ static unsigned int slot_for(int codepoint, unsigned int capacity) {
53
+ return ((unsigned int)codepoint * 2654435761u) & (capacity - 1);
54
+ }
55
+
56
+ /*
57
+ * The slot a codepoint occupies, or the first free one after where it would be.
58
+ *
59
+ * One walk answers both "is it here?" and "where does it go?", which is what
60
+ * lets insert find an existing entry and replace it rather than adding a
61
+ * second. Nothing is ever deleted, so there are no tombstones to skip and the
62
+ * walk stops at the first empty slot.
63
+ *
64
+ * Terminates because the table is never allowed to fill.
65
+ */
66
+ static rgame_glyph *probe(rgame_glyph *entries, unsigned int capacity, int codepoint) {
67
+ unsigned int slot = slot_for(codepoint, capacity);
68
+
69
+ for (;;) {
70
+ rgame_glyph *entry = &entries[slot];
71
+ if (entry->codepoint == 0 || entry->codepoint == codepoint) {
72
+ return entry;
73
+ }
74
+ slot = (slot + 1) & (capacity - 1);
75
+ }
76
+ }
77
+
78
+ /* Reallocates into a bigger table and re-probes every entry — slots depend on
79
+ * the capacity, so the old positions mean nothing in the new table. */
80
+ static int grow(rgame_glyph_cache *cache) {
81
+ unsigned int capacity = cache->capacity ? cache->capacity * 2
82
+ : RGAME_GLYPH_CACHE_MIN_CAPACITY;
83
+
84
+ rgame_glyph *entries = calloc(capacity, sizeof(rgame_glyph));
85
+ if (!entries) {
86
+ return 0;
87
+ }
88
+
89
+ for (unsigned int i = 0; i < cache->capacity; i++) {
90
+ if (cache->entries[i].codepoint != 0) {
91
+ *probe(entries, capacity, cache->entries[i].codepoint) = cache->entries[i];
92
+ }
93
+ }
94
+
95
+ free(cache->entries);
96
+ cache->entries = entries;
97
+ cache->capacity = capacity;
98
+ return 1;
99
+ }
100
+
101
+ int rgame_glyph_cache_find(const rgame_glyph_cache *cache, int codepoint, rgame_glyph *out) {
102
+ if (!cache || !out || !cache->entries || codepoint == 0) {
103
+ return 0;
104
+ }
105
+
106
+ const rgame_glyph *entry = probe(cache->entries, cache->capacity, codepoint);
107
+ if (entry->codepoint != codepoint) {
108
+ return 0;
109
+ }
110
+
111
+ *out = *entry;
112
+ return 1;
113
+ }
114
+
115
+ int rgame_glyph_cache_insert(rgame_glyph_cache *cache, const rgame_glyph *glyph) {
116
+ if (!cache || !glyph || glyph->codepoint == 0) {
117
+ return 0; /* 0 is the empty marker; storing it would hide the entry */
118
+ }
119
+
120
+ /* Grown *before* the insert, not after, so the table can never be full at
121
+ * the moment probe() walks it — that walk has no exit condition other than
122
+ * finding a free slot. */
123
+ if ((cache->count + 1) * RGAME_GLYPH_CACHE_LOAD_DENOMINATOR >=
124
+ cache->capacity * RGAME_GLYPH_CACHE_LOAD_NUMERATOR) {
125
+ if (!grow(cache)) {
126
+ return 0;
127
+ }
128
+ }
129
+
130
+ rgame_glyph *entry = probe(cache->entries, cache->capacity, glyph->codepoint);
131
+ /* An existing entry is overwritten in place, so the count only moves when
132
+ * the codepoint is genuinely new. */
133
+ if (entry->codepoint == 0) {
134
+ cache->count++;
135
+ }
136
+ *entry = *glyph;
137
+ return 1;
138
+ }
139
+
140
+ unsigned int rgame_glyph_cache_count(const rgame_glyph_cache *cache) {
141
+ return cache ? cache->count : 0;
142
+ }
@@ -0,0 +1,89 @@
1
+ #ifndef RGAME_GLYPH_CACHE_H
2
+ #define RGAME_GLYPH_CACHE_H
3
+
4
+ #include "graphics/clip.h"
5
+
6
+ /*
7
+ * What has already been rasterised, and where it went. Pure — no font, no
8
+ * atlas, no GL; this module knows only that a codepoint maps to a rectangle
9
+ * and some numbers.
10
+ *
11
+ * ---------------------------------------------------------------------------
12
+ * Per glyph, never per string
13
+ * ---------------------------------------------------------------------------
14
+ *
15
+ * A game draws a lot of short-lived, always-changing strings: a score, a timer,
16
+ * a coordinate readout. Caching those *as strings* would grow one entry per
17
+ * distinct string ever displayed, which for a timer is one per frame, forever.
18
+ * Caching per glyph bounds the whole thing by the character set instead — a few
19
+ * hundred entries for a Latin game, and it stops growing the moment the player
20
+ * has seen every digit.
21
+ *
22
+ * That is also why there is **no eviction**. There is nothing to evict: the
23
+ * cache cannot outgrow the glyph set the game actually draws, and dropping a
24
+ * glyph would only mean rasterising it again next frame. A cache with no
25
+ * eviction has no tombstones, no ages and no policy to get wrong, which is most
26
+ * of why this file is short.
27
+ *
28
+ * ---------------------------------------------------------------------------
29
+ * Open addressing
30
+ * ---------------------------------------------------------------------------
31
+ *
32
+ * One flat array, probed linearly, keyed by codepoint. No node per glyph, so
33
+ * lookup is a hash and a walk over adjacent memory — which matters because this
34
+ * is on the per-character path of every string drawn.
35
+ *
36
+ * Codepoint 0 marks an empty slot. That costs nothing: 0 is NUL, which is never
37
+ * a glyph anyone draws, and inserting it is refused rather than quietly
38
+ * corrupting the table.
39
+ */
40
+
41
+ /* One cached glyph: where its pixels are, and how to place them. */
42
+ typedef struct {
43
+ int codepoint;
44
+ int page; /* which atlas page holds it */
45
+ /* Where on that page, in pixels. Empty for a glyph with no ink, such as a
46
+ * space — which is still cached, because its advance is worth keeping. */
47
+ rgame_rect rect;
48
+
49
+ /* How far the pen moves after drawing it, in pixels at the font's size. */
50
+ float advance;
51
+ /* Where the pixels sit relative to the pen: x from the pen position, y from
52
+ * the *top of the line box* rather than the baseline, because that is the
53
+ * corner a caller passes to `text`. Converting once, here, keeps the
54
+ * baseline out of every drawing calculation downstream. */
55
+ float bearing_x, bearing_y;
56
+ } rgame_glyph;
57
+
58
+ typedef struct {
59
+ rgame_glyph *entries; /* NULL until the first insert */
60
+ unsigned int capacity; /* always a power of two, or 0 */
61
+ unsigned int count;
62
+ } rgame_glyph_cache;
63
+
64
+ /* An empty cache that has allocated nothing. A font that never draws text
65
+ * therefore costs nothing beyond this struct. */
66
+ void rgame_glyph_cache_init(rgame_glyph_cache *cache);
67
+
68
+ /* Frees the table. Safe on an empty cache, and safe to call twice. */
69
+ void rgame_glyph_cache_destroy(rgame_glyph_cache *cache);
70
+
71
+ /* Writes the cached glyph and returns 1, or returns 0 and leaves `out`
72
+ * untouched. */
73
+ int rgame_glyph_cache_find(const rgame_glyph_cache *cache, int codepoint, rgame_glyph *out);
74
+
75
+ /*
76
+ * Stores a glyph, replacing any entry for the same codepoint. Returns 1 on
77
+ * success and 0 if the codepoint is 0 or the table could not grow.
78
+ *
79
+ * A failed insert is not fatal to the caller: the glyph simply is not cached,
80
+ * and gets rasterised again next time. Slow, but correct — which is the right
81
+ * failure mode for a cache.
82
+ */
83
+ int rgame_glyph_cache_insert(rgame_glyph_cache *cache, const rgame_glyph *glyph);
84
+
85
+ /* How many distinct codepoints are cached. Bounded by the character set the
86
+ * game draws, which is the property the whole design rests on. */
87
+ unsigned int rgame_glyph_cache_count(const rgame_glyph_cache *cache);
88
+
89
+ #endif /* RGAME_GLYPH_CACHE_H */