Six findings, all of them libakgl's, all closed. The memcheck run is clean. Not one of the four json_load_file calls in src/ was ever matched by a json_decref, so every asset load abandoned its parsed document: 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on the order of a megabyte for a real one. Each loader now releases it in the CLEANUP block it already had, on the success path as well as the failure one. akgl_registry_load_properties needed its loop moved inside the ATTEMPT block first: props is a borrowed reference into the document and is read after the block ends, so every exit from that loop leaked the whole tree. akgl_get_property copied a fixed AKGL_MAX_STRING_LENGTH bytes out of what SDL handed back, which is SDL's strdup of the value -- four bytes for "0.0". That read up to 4 KiB past the end of somebody else's allocation on every property read, returned whatever was there past the terminator, and would have faulted on a value that landed at the end of a page. It copies the value and its terminator now, and refuses a value too long for an akgl_String rather than truncating it into an unterminated buffer. The header note that described the overread as a quirk describes correct behaviour instead. The four savegame name tables wrote a fixed-width field starting at the registry key, and SDL sizes that allocation to the name. They read past it on every entry and put what they found into the save file: up to half a kilobyte of this process's heap per registered object, in a file a player might send to somebody. They stage through a zeroed buffer now, and a negative-array-size typedef fails the build if a table's width ever outgrows it. akgl_controller_list_keyboards never freed the array SDL_GetKeyboards allocated for it. A font could be opened and published and never handed back -- there was no way to close one, so a game that changed fonts between scenes leaked ten kilobytes each time, and loading over a live name leaked the font it displaced. akgl_text_unloadfont is that missing half, and akgl_text_loadfont calls it when it replaces a name, after the new font has opened so a failed reload leaves the caller with the font they had. A new public symbol takes the version to 0.4.0 and the soname with it: an 0.3 consumer cannot be handed this library and told it is the same ABI. tests/registry.c fills a destination with a sentinel and asserts the bytes past the terminator survive a read, which fails against the old copy. tests/text.c covers unload, double unload, unloading a name that was never registered, and replacement closing the displaced font. The JSON releases have no test of their own and cannot sensibly have one -- nothing in the public API can observe a jansson refcount -- so the memcheck run is their test, which is an argument for gating it rather than against. The two remaining findings are in deps/semver's own unit test, which is vendored. They are suppressed by function name, so a rewrite of those cases comes back as a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
8.5 KiB
C
159 lines
8.5 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 _TEXT_H_
|
|
#define _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 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 `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
|
|
* `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_bind2d(); 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 // _TEXT_H_
|