Closes Defects items 20, 22, 23 and 29, and the residual of item 38. The tilemap load leak was five pooled strings per load; the property-lookup fix in an earlier commit took it to two, and the last two were each a claim with no matching release -- the string every layer's `type` was read into, and the dirname the map's relative paths resolve against. tests/tilemap.c asserts the pool is exactly where it started after one load/release cycle and after 64. Finding those two was a matter of dumping the contents of every still-claimed slot after a cycle rather than reading the code again; 'tilelayer' and an assets directory named themselves immediately. Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released its scratch string after reading each object's name and then kept using the slot, because akgl_get_json_string_value reuses a non-NULL destination without taking another reference. The slot was free while still live, so any other claim could have been handed it. akgl_character_sprite_add wrote over an existing binding without releasing the sprite it displaced, so a character that rebinds a state while alive leaked a sprite slot per rebind -- teardown only gives back what the map holds at the end. The new reference is taken before the write and given back if the write fails, so there is no window where a sprite is bound with nothing behind it. The write was unchecked too. Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two pooled strings, akgl_controller_open_gamepads' enumeration array, and akgl_text_rendertextat's surface and texture -- the last being a leak per frame on a HUD line. akgl_text_unloadallfonts() closes every font in the registry and destroys it, which is what item 38 left open. Deliberately not a whole akgl_game_shutdown: tearing down the mixer, SDL_ttf and SDL in the right order is a design question, and this is the part that was simply missing. It is a new public symbol, which 0.5.0 already covers -- this release has not shipped. Every fix has a test that fails against the old code. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol all clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
9.4 KiB
C
177 lines
9.4 KiB
C
/**
|
|
* @file text.h
|
|
* @brief Loading fonts and drawing or measuring strings with them.
|
|
*
|
|
* Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property
|
|
* registry under a caller-chosen name; there is no akgl font type wrapping them.
|
|
* SDL_ttf must be initialized (akgl_game_init does it) before any of this.
|
|
*
|
|
* The two measure functions do not touch the renderer, so they are usable
|
|
* before -- or entirely without -- a window. Drawing is immediate mode: each
|
|
* akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the
|
|
* texture away, which is fine for a HUD line and wrong for a large body of
|
|
* static text redrawn every frame.
|
|
*/
|
|
|
|
#ifndef _AKGL_TEXT_H_
|
|
#define _AKGL_TEXT_H_
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_ttf/SDL_ttf.h>
|
|
#include <akerror.h>
|
|
|
|
/**
|
|
* @brief Open a TrueType font at one size and publish it in the font registry.
|
|
*
|
|
* A size is baked into the handle, so the same file at two sizes is two calls
|
|
* under two names. Nothing releases these: the handles live until the process
|
|
* ends.
|
|
*
|
|
* @param name Registry key to publish the font under. Required. An existing
|
|
* entry with the same name is replaced, and the font it
|
|
* displaced is closed through akgl_text_unloadfont -- but only
|
|
* after the new one has opened, so a failed load leaves the
|
|
* caller with the font they already had.
|
|
* @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not
|
|
* resolved against `SDL_GetBasePath()`.
|
|
* @param size Point size to rasterize at. Passed straight to SDL_ttf, which
|
|
* rejects anything that is not positive.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`.
|
|
* @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not
|
|
* a font, or a @p size SDL_ttf refuses. The message carries
|
|
* `SDL_GetError()`.
|
|
* @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT --
|
|
* in practice, because akgl_registry_init has not run.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
|
/**
|
|
* @brief Close a loaded font and take it out of the font registry.
|
|
*
|
|
* The other half of akgl_text_loadfont, and for a while the half that did not
|
|
* exist: a font could be opened and published but never handed back, so a game
|
|
* that changed fonts between scenes had no way to reclaim the one it had
|
|
* finished with. A `TTF_Font` is about ten kilobytes once FreeType's own
|
|
* structures are counted.
|
|
*
|
|
* The registry entry is cleared before the font is closed, so a font is never
|
|
* reachable through #AKGL_REGISTRY_FONT after it has gone back to SDL_ttf.
|
|
*
|
|
* @param name Registry key the font was published under. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p name is `NULL`.
|
|
* @throws AKERR_KEY If no font is registered under @p name -- including the
|
|
* case where it was already unloaded, which makes a double unload an
|
|
* error rather than a double close.
|
|
*
|
|
* @warning Anything still holding the `TTF_Font *` -- a caller that fetched it
|
|
* from the registry earlier, a pending akgl_text_rendertextat -- is
|
|
* left with a dangling pointer. Fonts are not reference counted.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
|
|
/**
|
|
* @brief Close every font in #AKGL_REGISTRY_FONT and destroy the registry.
|
|
*
|
|
* A font, once loaded, was reachable only by name -- so a game that exited
|
|
* without unloading each one by hand left them open, and `SDL_Quit` destroying
|
|
* the property registry took the last reference to them with it. That is
|
|
* bounded by how many fonts a game loads rather than unbounded, but there was
|
|
* no way to do anything about it at all.
|
|
*
|
|
* Call this during shutdown, **before** `TTF_Quit` or `SDL_Quit`. Afterwards
|
|
* #AKGL_REGISTRY_FONT is 0 and akgl_text_loadfont needs
|
|
* akgl_registry_init_font() again before it can register anything.
|
|
*
|
|
* @return `NULL`. Closing a font reports nothing, and an uninitialized registry
|
|
* is success rather than an error -- shutdown paths run after partial
|
|
* startups.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadallfonts(void);
|
|
/**
|
|
* @brief Rasterize a string and blit it at a screen position, in one call.
|
|
*
|
|
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
|
|
* result to a texture, draws it through the global `akgl_renderer`, and destroys both
|
|
* the texture and the surface before returning. The text is drawn at its natural
|
|
* size -- @p x and @p y are the top-left corner, not a centre.
|
|
*
|
|
* Coordinates are screen coordinates, not world ones: this does not go through
|
|
* the camera, so a HUD stays put while the world scrolls under it.
|
|
*
|
|
* @param font Font to render with, from akgl_text_loadfont. Required.
|
|
* @param text UTF-8 text. Required. May contain newlines, which break
|
|
* lines on either path.
|
|
* @param color Text colour, including alpha.
|
|
* @param wraplength Wrap width in pixels. Greater than 0 wraps on word
|
|
* boundaries at that width; 0 or less draws a single line and
|
|
* breaks only on newlines in @p text.
|
|
* @param x Left edge of the text, in screen pixels.
|
|
* @param y Top edge of the text, in screen pixels.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
|
|
* `akgl_renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
|
|
* that last one is the state a backend is in between being allocated
|
|
* and being run through akgl_render_2d_bind(); if SDL_ttf cannot
|
|
* rasterize the string; or if the surface cannot be uploaded as a
|
|
* texture. The last two carry `SDL_GetError()` and are a reused status
|
|
* rather than a pointer problem.
|
|
* @throws AKERR_* Whatever the backend's `draw_texture` raises.
|
|
*
|
|
* @note On a failure after rasterizing -- the texture upload, or the draw -- the
|
|
* surface and texture are not destroyed, because the error returns before
|
|
* the cleanup. Repeated failures leak.
|
|
* @note The empty string is **refused**, not drawn as nothing: SDL_ttf reports
|
|
* "Text has zero width" and this passes that on as `AKERR_NULLPOINTER`.
|
|
* akgl_text_measure() accepts it, so the two disagree. A caller drawing a
|
|
* line of text that may be empty has to check for it. TODO.md, "Known and
|
|
* still open".
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
|
/**
|
|
* @brief Report the size, in pixels, that @p text would occupy on one line.
|
|
*
|
|
* Nothing is drawn and no renderer is required. A caller building a character
|
|
* grid measures one cell with this -- the advance width of a single glyph in a
|
|
* monospaced font -- and derives the rest of the grid from it.
|
|
*
|
|
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
|
* @param text UTF-8 text to measure. Required. The empty string is legal and
|
|
* measures 0 wide by one line high.
|
|
* @param w Receives the width in pixels. Required.
|
|
* @param h Receives the height in pixels -- one line, whatever @p text
|
|
* contains, since this form does not wrap. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
|
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
|
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
|
|
/**
|
|
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
|
|
*
|
|
* The companion to akgl_text_measure() for the wrapping case, matching the
|
|
* @p wraplength argument akgl_text_rendertextat() already takes: a string
|
|
* longer than @p wraplength reports the height of every line it breaks onto.
|
|
* A @p wraplength of zero wraps only on newlines in @p text.
|
|
*
|
|
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
|
* @param text UTF-8 text to measure. Required.
|
|
* @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is
|
|
* refused rather than passed through: SDL_ttf reads a negative
|
|
* width as a very large unsigned one and silently stops
|
|
* wrapping, which would return a measurement that is wrong
|
|
* rather than an error.
|
|
* @param w Receives the width in pixels: the longest line, not
|
|
* @p wraplength. Required.
|
|
* @param h Receives the height in pixels, covering every line the text
|
|
* wraps onto. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
|
* @throws AKERR_OUTOFBOUNDS If @p wraplength is negative.
|
|
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
|
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
|
|
|
|
#endif // _AKGL_TEXT_H_
|