I don't have any experience with PS1 development, but I've been working on making games in C for the Playdate recently, and here's some things I've found useful:
when it comes to memory management, don't "OCD malloc()/free()"—take a second and think about the structure of your game, and plan things out accordingly. is your game going to have different levels/maps, each with different assets (map data, textures, meshes) that need to be loaded/unloaded between levels/maps?
you can use an arena allocator to allocate a big ol chunk of memory for the current level (char* total_memory = malloc(1024*1024*2)), which can then be reset without free()ing by just setting its high water mark (a size_t) to zero. you can also use an arena allocator to provide yourself a temporary, per-frame buffer—just reset its high water mark to zero at the end of each frame—when you have this, who needs garbage collection?
here's a slightly modified version of what I'm using in my Playdate games:
#define TOTAL_MEMORY 1024 * 1024 * 1 // 1MB of memory (out of 4, I think, on a PS1?)
typedef enum {
ARENA_GAME,
ARENA_LEVEL,
ARENA_FRAME,
MEM_ARENA_COUNT
} Mem_Arena;
char* mem_chunk;
Mem_Arena mem_arena = ARENA_GAME;
size_t mem_end[MEM_ARENA_COUNT] = { 0 };
void mem_init() { mem_chunk = malloc(TOTAL_MEMORY); }
void* mem_alloc(size_t bytes) {
void* ptr = mem_chunk + mem_end[mem_arena];
mem_end[mem_arena] += bytes;
return ptr;
}
void mem_reset() {
for (int i = mem_arena; i < MEM_ARENA_COUNT; ++i)
mem_end[i] = (i == 0) ? 0 : mem_end[i-1];
}
void mem_use(Mem_Arena arena) {
for (int i = 0; i <= arena; ++i)
if (mem_end[i] == 0) mem_end[i] = (i == 0) ? 0 : mem_end[i-1];
mem_arena = arena;
mem_reset();
}
mem_init() at the start of the game, mem_alloc() to allocate memory in the current arena (ARENA_GAME at the start), mem_use(ARENA_LEVEL) when done loading game-wide stuff, mem_use(ARENA_FRAME) when done loading level stuff, mem_reset() at the end of the frame loop. to unload one level and load another, simply mem_use(ARENA_LEVEL), and then mem_use(ARENA_FRAME) once again when done.
if you don't need per-level assets, just remove ARENA_LEVEL from the enum, and mem_use(ARENA_FRAME) after your initial game-wide loading stuff.
instead of dynamically-allocated and/or resizable arrays, consider fixed-size arrays with a size_t indicating which element can be used next, which is also the count of the elements currently in the array (starting at zero). to add an element, store it in foos[foos_count++]. to remove element n from the array, swap it with the last element of the array, and decrement the size_t.
with a little bit of forethought, you shouldn't really ever have to free() memory for a simple game. (but, again, I know nothing about how the PS1 works—maybe you actually do need to free() stuff, for some reason. I doubt it though.)
check out this article for more inspiration: https://phoboslab.org/log/2023/08/rewriting-wipeout
in general, just create data structures and write functions that operate on them. don't get fancy with abstractions—keep everything as simple as possible.