Give back every pooled string and sprite reference the loaders take
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>
This commit is contained in:
@@ -44,13 +44,37 @@ akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Spr
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
char stateval[32];
|
||||
akgl_Sprite *displaced = NULL;
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
|
||||
FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
|
||||
memset(&stateval, 0x00, 32);
|
||||
SDL_itoa(state, (char *)&stateval, 10);
|
||||
SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref);
|
||||
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
|
||||
|
||||
// Whatever this state was bound to is losing its binding, and the reference
|
||||
// taken for that binding with it. Without this a character that rebinds a
|
||||
// state while alive leaks one sprite slot per rebind -- releasing at
|
||||
// teardown only gives back whatever the map happens to hold at the end.
|
||||
displaced = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
|
||||
|
||||
// Take the new reference before anything can fail, so a failed write cannot
|
||||
// leave the sprite bound with no reference behind it.
|
||||
ref->refcount += 1;
|
||||
if ( SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref) == false ) {
|
||||
ref->refcount -= 1;
|
||||
FAIL_RETURN(
|
||||
errctx,
|
||||
AKERR_KEY,
|
||||
"Unable to bind sprite %s to character %s for state %d: %s",
|
||||
(char *)&ref->name,
|
||||
(char *)&basechar->name,
|
||||
state,
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
if ( (displaced != NULL) && (displaced != ref) ) {
|
||||
PASS(errctx, akgl_heap_release_sprite(displaced));
|
||||
}
|
||||
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -212,19 +212,41 @@ akerr_ErrorContext *akgl_controller_open_gamepads(void)
|
||||
int i = 0;
|
||||
SDL_JoystickID *gamepads = NULL;
|
||||
SDL_Gamepad *gamepad = NULL;
|
||||
bool openfailed = false;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
if ( SDL_HasGamepad() ) {
|
||||
gamepads = SDL_GetGamepads(&count);
|
||||
if ( count > 0 ) {
|
||||
FAIL_ZERO_RETURN(errctx, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
for ( i = 0; i < count ; i++ ) {
|
||||
gamepad = SDL_OpenGamepad(gamepads[i]);
|
||||
FAIL_ZERO_RETURN(errctx, gamepad, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
|
||||
}
|
||||
SDL_free(gamepads);
|
||||
// SDL_free in CLEANUP, not after the loop. A gamepad that failed to
|
||||
// open used to return straight out of here and take the enumeration
|
||||
// array with it.
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
for ( i = 0; i < count ; i++ ) {
|
||||
gamepad = SDL_OpenGamepad(gamepads[i]);
|
||||
if ( gamepad == NULL ) {
|
||||
openfailed = true;
|
||||
break;
|
||||
}
|
||||
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
|
||||
}
|
||||
// Reported after the loop rather than inside it: a
|
||||
// FAIL_ZERO_BREAK in there would break the loop, not the block.
|
||||
FAIL_NONZERO_BREAK(errctx, openfailed, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
} CLEANUP {
|
||||
if ( gamepads != NULL ) {
|
||||
SDL_free(gamepads);
|
||||
gamepads = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
} else {
|
||||
// SDL_GetGamepads allocates even when it reports none.
|
||||
if ( gamepads != NULL ) {
|
||||
SDL_free(gamepads);
|
||||
gamepads = NULL;
|
||||
}
|
||||
SDL_Log("No gamepads enumerated");
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,13 +23,26 @@ akerr_ErrorContext *akgl_render_2d_init(akgl_RenderBackend *self)
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
PASS(errctx, akgl_get_property("game.screenwidth", &width, "0"));
|
||||
PASS(errctx, akgl_get_property("game.screenheight", &height, "0"));
|
||||
PASS(errctx, aksl_atoi(width->data, &screenwidth));
|
||||
PASS(errctx, aksl_atoi(height->data, &screenheight));
|
||||
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
|
||||
PASS(errctx, akgl_heap_release_string(width));
|
||||
PASS(errctx, akgl_heap_release_string(height));
|
||||
// The two pooled strings come back in CLEANUP. Released after both parses,
|
||||
// as they used to be, a non-numeric game.screenwidth returned past them and
|
||||
// leaked two of the pool's 256 entries.
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_get_property("game.screenwidth", &width, "0"));
|
||||
CATCH(errctx, akgl_get_property("game.screenheight", &height, "0"));
|
||||
CATCH(errctx, aksl_atoi(width->data, &screenwidth));
|
||||
CATCH(errctx, aksl_atoi(height->data, &screenheight));
|
||||
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
|
||||
} CLEANUP {
|
||||
if ( width != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(width));
|
||||
width = NULL;
|
||||
}
|
||||
if ( height != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(height));
|
||||
height = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
|
||||
60
src/text.c
60
src/text.c
@@ -54,6 +54,38 @@ akerr_ErrorContext *akgl_text_unloadfont(char *name)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SDL_EnumerateProperties callback: close one registered font.
|
||||
*
|
||||
* The properties set cannot be modified while it is being enumerated, so this
|
||||
* only closes; akgl_text_unloadallfonts clears the whole set afterwards.
|
||||
*/
|
||||
static void unload_font_iterate(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
if ( name == NULL ) {
|
||||
return;
|
||||
}
|
||||
font = (TTF_Font *)SDL_GetPointerProperty(props, name, NULL);
|
||||
if ( font != NULL ) {
|
||||
TTF_CloseFont(font);
|
||||
}
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_text_unloadallfonts(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
if ( AKGL_REGISTRY_FONT == 0 ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
SDL_EnumerateProperties(AKGL_REGISTRY_FONT, &unload_font_iterate, NULL);
|
||||
SDL_DestroyProperties(AKGL_REGISTRY_FONT);
|
||||
AKGL_REGISTRY_FONT = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
|
||||
{
|
||||
SDL_Surface *textsurf = NULL;
|
||||
@@ -85,14 +117,26 @@ akerr_ErrorContext *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color
|
||||
color);
|
||||
}
|
||||
FAIL_ZERO_RETURN(errctx, textsurf, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
texture = SDL_CreateTextureFromSurface(akgl_renderer->sdl_renderer, textsurf);
|
||||
FAIL_ZERO_RETURN(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
dest.x = x;
|
||||
dest.y = y;
|
||||
SDL_GetTextureSize(texture, &dest.w, &dest.h);
|
||||
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
SDL_DestroyTexture(texture);
|
||||
SDL_DestroySurface(textsurf);
|
||||
|
||||
// Both were destroyed only after the draw succeeded, so a failed texture
|
||||
// upload leaked the surface and a failed draw leaked both -- on a HUD line
|
||||
// redrawn every frame, that is a leak per frame.
|
||||
ATTEMPT {
|
||||
texture = SDL_CreateTextureFromSurface(akgl_renderer->sdl_renderer, textsurf);
|
||||
FAIL_ZERO_BREAK(errctx, texture, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
dest.x = x;
|
||||
dest.y = y;
|
||||
SDL_GetTextureSize(texture, &dest.w, &dest.h);
|
||||
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
} CLEANUP {
|
||||
if ( texture != NULL ) {
|
||||
SDL_DestroyTexture(texture);
|
||||
texture = NULL;
|
||||
}
|
||||
SDL_DestroySurface(textsurf);
|
||||
textsurf = NULL;
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -322,41 +322,58 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
|
||||
PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
|
||||
len = json_array_size((json_t *)layerdata);
|
||||
curlayer = &dest->layers[layerid];
|
||||
for ( j = 0; j < len; j++ ) {
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Object layer %d has more than %d objects",
|
||||
layerid,
|
||||
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
|
||||
);
|
||||
PASS(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
|
||||
curobj = &curlayer->objects[j];
|
||||
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
|
||||
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
|
||||
PASS(errctx, akgl_heap_release_string(tmpstr));
|
||||
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
|
||||
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
|
||||
PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
|
||||
|
||||
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
|
||||
if ( strcmp(tmpstr->data, "actor") == 0 ) {
|
||||
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
|
||||
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
|
||||
curobj->visible = false;
|
||||
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
|
||||
dest->p_foreground_y = curobj->y;
|
||||
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
|
||||
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
|
||||
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
|
||||
dest->p_vanishing_y = curobj->y;
|
||||
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
|
||||
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
|
||||
// The loop is the whole ATTEMPT body on purpose. A CATCH inside a loop
|
||||
// breaks the loop rather than the block, which is only safe because there
|
||||
// is nothing after it -- CLEANUP, PROCESS and FINISH still run and still
|
||||
// propagate. Anything added after this loop has to account for that.
|
||||
ATTEMPT {
|
||||
for ( j = 0; j < len; j++ ) {
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Object layer %d has more than %d objects",
|
||||
layerid,
|
||||
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
|
||||
);
|
||||
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
|
||||
curobj = &curlayer->objects[j];
|
||||
// One scratch string for the whole walk, released once in CLEANUP.
|
||||
// Releasing it after the name read -- which is what this used to do
|
||||
// -- dropped its refcount to zero while the very next line went on
|
||||
// using the same slot, because akgl_get_json_string_value reuses a
|
||||
// non-NULL destination without taking another reference. Any other
|
||||
// claim in between would have been handed the same string.
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
|
||||
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
|
||||
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
|
||||
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
|
||||
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
|
||||
if ( strcmp(tmpstr->data, "actor") == 0 ) {
|
||||
CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
|
||||
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
|
||||
curobj->visible = false;
|
||||
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
|
||||
dest->p_foreground_y = curobj->y;
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
|
||||
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
|
||||
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
|
||||
dest->p_vanishing_y = curobj->y;
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
|
||||
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
|
||||
}
|
||||
}
|
||||
layerdatavalue = NULL;
|
||||
}
|
||||
layerdatavalue = NULL;
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( tmpstr != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -471,6 +488,12 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
|
||||
layerid += 1;
|
||||
}
|
||||
} CLEANUP {
|
||||
// One scratch string for every layer's type, given back once. It was
|
||||
// claimed by the first accessor call and never released, which is one
|
||||
// of the two pool strings a map load used to keep for good.
|
||||
if ( tmpstr != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -618,7 +641,6 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||
SDL_Log("Map perspective rate is %f", dest->p_rate);
|
||||
}
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
// The map is built entirely out of copies -- layer data, tileset
|
||||
// geometry, object names -- so the document is dead the moment the
|
||||
// loaders above return, whether they succeeded or not. It is also the
|
||||
@@ -627,6 +649,13 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
// And the directory the map's relative paths resolve against, which
|
||||
// was claimed above and never released -- the other of the two pool
|
||||
// strings a map load kept.
|
||||
if ( dirnamestr != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(dirnamestr));
|
||||
dirnamestr = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
Reference in New Issue
Block a user