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,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 */
|