diff --git a/OTRExporter b/OTRExporter index 32e088e28c8..1d1a069be7e 160000 --- a/OTRExporter +++ b/OTRExporter @@ -1 +1 @@ -Subproject commit 32e088e28c8cdd055d4bb8f3f219d33ad37963f3 +Subproject commit 1d1a069be7e7f2a4c7bea1ad6c28b5586b3e3a6b diff --git a/ZAPDTR b/ZAPDTR index ee3397a365c..3310bd2ecd0 160000 --- a/ZAPDTR +++ b/ZAPDTR @@ -1 +1 @@ -Subproject commit ee3397a365c5f350a60538c88f0643f155944836 +Subproject commit 3310bd2ecd090666d94f3fae4a9fc68e6fa7efbe diff --git a/soh/include/functions.h b/soh/include/functions.h index 54f2c2b5dde..be0cd78da61 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -1023,6 +1023,10 @@ void ZeldaArena_FreeDebug(void* ptr, const char* file, s32 line); void* ZeldaArena_Calloc(size_t num, size_t size); void ZeldaArena_Display(); void ZeldaArena_GetSizes(u32* outMaxFree, u32* outFree, u32* outAlloc); + +// [SOH] Enhancement - Heap Viewer +ArenaNode* ZeldaArena_GetHead(void); + void ZeldaArena_Check(); void ZeldaArena_Init(void* start, size_t size); void ZeldaArena_Cleanup(); diff --git a/soh/soh/ActorDB.cpp b/soh/soh/ActorDB.cpp index 70ea29675e6..bf6e0b50c5f 100644 --- a/soh/soh/ActorDB.cpp +++ b/soh/soh/ActorDB.cpp @@ -17,10 +17,15 @@ ActorDB* ActorDB::Instance; struct AddPair { const char* name; ActorInit& init; + // #region SOH [Enhancement] - Hardware Memory Limits + AllocType allocType; + // #endregion }; -#define DEFINE_ACTOR_INTERNAL(name, _1, allocType) { #name, name##_InitVars }, -#define DEFINE_ACTOR(name, _1, allocType) { #name, name##_InitVars }, +// #region SOH [Enhancement] - Hardware Memory Limits +#define DEFINE_ACTOR_INTERNAL(name, _1, allocType) { #name, name##_InitVars, allocType }, +#define DEFINE_ACTOR(name, _1, allocType) { #name, name##_InitVars, allocType }, +// #endregion #define DEFINE_ACTOR_UNSET(_0) static constexpr AddPair initialActorTable[] = { @@ -470,6 +475,9 @@ ActorDB::ActorDB() { db.reserve(ACTOR_NUMBER_MAX); // reserve size for all initial entries so we don't do it for each for (const AddPair& pair : initialActorTable) { Entry& entry = AddEntry(pair.name, actorDescriptions[pair.init.id], pair.init); + // #region SOH [Enhancement] - Hardware Memory Limits + entry.entry.allocType = pair.allocType; + // #endregion } } diff --git a/soh/soh/ActorDB.h b/soh/soh/ActorDB.h index dd8ecf90153..b65d23fe95c 100644 --- a/soh/soh/ActorDB.h +++ b/soh/soh/ActorDB.h @@ -16,6 +16,9 @@ typedef struct { ActorFunc draw; ActorResetFunc reset; s32 numLoaded; + // #region SOH [Enhancement] - Hardware Memory Limits + AllocType allocType; + // #endregion } ActorDBEntry; #ifdef __cplusplus diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp index 04514b35205..c7e89d7eb06 100644 --- a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp +++ b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp @@ -9,6 +9,7 @@ #include "soh/ResourceManagerHelpers.h" #include "soh/SohGui/MenuTypes.h" #include "soh/SohGui/SohMenu.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" extern "C" { #include @@ -458,9 +459,19 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params; EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play); + // #region SOH [Enhancement] - Hardware Memory Limits + // Save the original actor ID before randomization swap it. The shadow heap needs the original ID to allocate + // the correct overlay and instance sizes. + const s16 originalActorId = *actorId; + // #endregion + *actorId = randomEnemy.id; *params = randomEnemy.params; + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_SetOriginalActorId(originalActorId); + // #endregion + // Straighten out enemies so they aren't flipped on their sides when the original spawn is. *rotX = 0; @@ -986,4 +997,4 @@ void RegisterEnemyRandomizerWidgets() { } static RegisterShipInitFunc initFunc(RegisterEnemyRandomizer, { CVAR_ENEMY_RANDOMIZER_NAME }); -static RegisterMenuInitFunc menuInitFunc(RegisterEnemyRandomizerWidgets); +static RegisterMenuInitFunc menuInitFunc(RegisterEnemyRandomizerWidgets); \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.cpp b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.cpp new file mode 100644 index 00000000000..3ff21bc736e --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.cpp @@ -0,0 +1,594 @@ +#include + +#include +#include + +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" +#include "soh/ActorDB.h" + +extern "C" { +#include "HardwareMemoryLimits.hpp" +#include "N64SizeData.hpp" +#include "n64_shadow_arena.h" +#include "n64_arena_sizing.h" +} + +#define CVAR_NAME CVAR_ENHANCEMENT("HardwareMemoryLimits") +#define CVAR_DEFAULT 0 +#define CVAR_VALUE CVarGetInteger(CVAR_NAME, CVAR_DEFAULT) + +// -------------------------------------------------------------------------------------------------------------------- +// State +// -------------------------------------------------------------------------------------------------------------------- + +static int32_t sIsActive = 0; +static ShadowArena sShadow = {}; +static uint32_t sSohThaRemainder = 0; +static uint8_t sElfMsgNum = 0; + +// Shadow offsets for actor overlays, keyed by actor ID. SHADOW_NULL means no shadow allocation exists for that type. +static uint32_t sOverlayShadows[ACTOR_ID_MAX]; + +// Shadow offsets for effect overlays, keyed by effect type. +static uint32_t sEffectOverlayShadows[EFFECT_SS_TYPE_MAX]; + +// Shadow offset for the shared absolute-space overlay buffer. +static uint32_t sAbsoluteSpaceShadow = SHADOW_NULL; + +// Maps real pointers (instances and subsidiaries) to their shadow offsets. +static std::unordered_map sShadowMap; + +// Maps real actor pointers to their original actor ID (before enemy randomizer substitution). Used by FreeOverlay to +// free the correct overlay shadow entry. Only populated for randomized actors. +static std::unordered_map sActorOriginalIds; + +// Block metadata for the heap viewer. Keyed by shadow DATA offset (node offset + arena->nodeSize). +// Populated in each Alloc function, erased in each Free, cleared in N64Mem_Reset. +struct BlockMeta { + uint8_t type; + int16_t actorId; +}; + +static std::unordered_map sBlockMetaMap; + +// When the enemy randomizer replaces an actor, this holds the ORIGINAL actor ID so that shadow allocations are charged +// at the original N64 size rather than the replacement's size. Set by N64Mem_SetOriginalActorId before Actor_Spawn, +// cleared by N64Mem_ClearOriginalActorId after Actor_Spawn returns. -1 means no override (use the passed actorId). +static int16_t sOriginalActorId = -1; + +// Set when AllocInstance consumes a non-negative sOriginalActorId, cleared after Actor_Init returns. While set, ALL +// shadow allocations (overlay, instance, subsidiary) from child actors spawned during the replacement actor's init are +// skipped -- on N64 these children don't exist because the original actor never spawned them. +static int32_t sIsInsideRandomizedInit = 0; + +// Returns the actor ID to use for size lookups. If an original actor ID override is set (enemy randomizer active), +// returns that; otherwise returns the passed actorId unchanged. +static uint16_t ResolveSizeActorId(int16_t actorId) { + return sOriginalActorId >= 0 ? static_cast(sOriginalActorId) : static_cast(actorId); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Diagnostics +// -------------------------------------------------------------------------------------------------------------------- + +static int32_t sTraceEnabled = 0; + +static void LogShadowState(const char* context) { + uint32_t maxFree = 0; + uint32_t totalFree = 0; + uint32_t totalAlloc = 0; + + ShadowArena_GetSizes(&sShadow, &maxFree, &totalFree, &totalAlloc); + SPDLOG_INFO("[HardwareMemoryLimits] ({}): alloc=0x{:X}, free=0x{:X}, largest=0x{:X}", context, totalAlloc, + totalFree, + maxFree); +} + +static void TraceAlloc(const char* tag, uint32_t id, uint32_t size) { + if (sTraceEnabled) { + uint32_t consumed = (size + 0xF & ~0xF) + sShadow.nodeSize; + SPDLOG_TRACE("[N64Trace] +{} id=0x{:X} sz=0x{:X} cost=0x{:X}", tag, id, size, consumed); + } +} + +static void TraceFree(const char* tag, uint32_t id) { + if (sTraceEnabled) { + SPDLOG_TRACE("[N64Trace] -{} id=0x{:X}", tag, id); + } +} + +// -------------------------------------------------------------------------------------------------------------------- +// Graveyard benchmark +// -------------------------------------------------------------------------------------------------------------------- + +static int32_t sGraveyardTransitionCount = 0; + +void N64Mem_BenchmarkTransition(PlayState* play) { + if (!sIsActive || play->sceneNum != SCENE_GRAVEYARD) { + return; + } + + sGraveyardTransitionCount++; + + uint32_t maxFree = 0; + uint32_t totalFree = 0; + uint32_t totalAlloc = 0; + ShadowArena_GetSizes(&sShadow, &maxFree, &totalFree, &totalAlloc); + + SPDLOG_INFO("[N64Benchmark] transition={}, largest_free=0x{:X}, total_free=0x{:X}", + sGraveyardTransitionCount, maxFree, totalFree); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Lifecycle +// -------------------------------------------------------------------------------------------------------------------- + +void N64Mem_StoreThaRemainder(uint32_t sohRemainder) { + sSohThaRemainder = sohRemainder; +} + +void N64Mem_StoreElfMsgNum(uint8_t num) { + sElfMsgNum = num; +} + +uint8_t N64Mem_GetElfMsgNum() { + return sElfMsgNum; +} + +void N64Mem_Reset(PlayState* play) { + // Log shadow state before teardown for per-scene diagnostics. + if (sIsActive && sShadow.buffer) { + uint32_t maxFree = 0; + uint32_t totalFree = 0; + uint32_t totalAlloc = 0; + + ShadowArena_GetSizes(&sShadow, &maxFree, &totalFree, &totalAlloc); + SPDLOG_INFO("[HardwareMemoryLimits] Teardown: alloc=0x{:X}, free=0x{:X}, largest=0x{:X}, ptrs={}", totalAlloc, + totalFree, maxFree, sShadowMap.size()); + + if (sGraveyardTransitionCount > 0) { + SPDLOG_INFO("[N64Benchmark] RESULT transitions={}", sGraveyardTransitionCount); + } + } + + sGraveyardTransitionCount = 0; + + // Tear down previous shadow state unconditionally -- the real ZeldaArena has already been reinitialized by + // Play_Init. + ShadowArena_Destroy(&sShadow); + sShadowMap.clear(); + sActorOriginalIds.clear(); + sBlockMetaMap.clear(); + sAbsoluteSpaceShadow = SHADOW_NULL; + sOriginalActorId = -1; + sIsInsideRandomizedInit = 0; + + for (uint32_t& sOverlayShadow : sOverlayShadows) { + sOverlayShadow = SHADOW_NULL; + } + + for (uint32_t& sEffectOverlayShadow : sEffectOverlayShadows) { + sEffectOverlayShadow = SHADOW_NULL; + } + + sIsActive = CVAR_VALUE; + if (sIsActive && play != nullptr) { + // Compute N64-equivalent arena size from first principles. All per-version constants are derived from the OTR + // blob, which was extracted from the user's specific ROM version. + uint32_t shadowArenaSize = ArenaSizing_ComputeN64ArenaSize(play); + if (shadowArenaSize == 0) { + SPDLOG_ERROR("[HardwareMemoryLimits] Arena sizing returned 0 -- THA budget exceeded, disabling."); + sIsActive = 0; + return; + } + + SPDLOG_INFO("[HardwareMemoryLimits] Shadow arena size=0x{:X} for scene 0x{:X}", shadowArenaSize, + play->sceneNum); + ShadowArena_Init(&sShadow, shadowArenaSize, N64SizeData_GetArenaNodeSize()); + + sTraceEnabled = play->sceneNum == SCENE_GRAVEYARD; + } +} + +int32_t N64Mem_IsActive() { + return sIsActive; +} + +ShadowArena* N64Mem_GetShadowArena() { + return &sShadow; +} + +void N64Mem_LogState(const char* context) { + if (sIsActive) { + LogShadowState(context); + } +} + +void N64Mem_DumpArena(const char* tag) { + if (!sIsActive || sShadow.buffer == nullptr) { + return; + } + + std::string out = fmt::format("\n[N64HeapDump] === {} ===\n", tag); + + uint32_t offset = ShadowArena_GetHead(&sShadow); + uint32_t freeCount = 0; + uint32_t allocCount = 0; + uint32_t freeTotal = 0; + uint32_t allocTotal = 0; + + while (offset != SHADOW_NULL) { + int32_t isFree = 0; + uint32_t size = 0; + uint32_t next = 0; + if (!ShadowArena_GetNodeInfo(&sShadow, offset, &isFree, &size, &next)) { + out += fmt::format(" +0x{:06X} \n", offset); + break; + } + + const uint32_t dataOff = offset + sShadow.nodeSize; + if (isFree) { + out += fmt::format(" +0x{:06X} 0x{:06X} FREE\n", offset, size); + freeTotal += size; + freeCount++; + } else { + uint8_t type = 0; + int16_t actorId = -1; + auto typeName = "???"; + if (N64Mem_GetBlockInfo(dataOff, &type, &actorId)) { + switch (type) { + case N64MEM_BLOCK_INSTANCE: + typeName = "inst"; + break; + case N64MEM_BLOCK_OVERLAY: + typeName = "ovl "; + break; + case N64MEM_BLOCK_SUBSIDIARY: + typeName = "sub "; + break; + case N64MEM_BLOCK_EFFECT: + typeName = "efx "; + break; + case N64MEM_BLOCK_ABSOLUTE: + typeName = "abs "; + break; + default: + break; + } + } + + if (actorId >= 0) { + out += fmt::format(" +0x{:06X} 0x{:06X} {} actor=0x{:04X}\n", offset, size, typeName, + static_cast(actorId)); + } else { + out += fmt::format(" +0x{:06X} 0x{:06X} {}\n", offset, size, typeName); + } + allocTotal += size; + allocCount++; + } + + offset = next; + } + + out += fmt::format("[N64HeapDump] {} blocks: {} alloc (0x{:X}B), {} free (0x{:X}B)", freeCount + allocCount, + allocCount, allocTotal, freeCount, freeTotal); + + SPDLOG_INFO("{}", out); +} + +void N64Mem_SetOriginalActorId(int16_t actorId) { + sOriginalActorId = actorId; +} + +void N64Mem_ClearOriginalActorId() { + sOriginalActorId = -1; +} + +int32_t N64Mem_IsRandomizedActor(void* realPtr) { + if (!sIsActive || !realPtr) { + return 0; + } + + return sActorOriginalIds.contains(realPtr) ? 1 : 0; +} + +int32_t N64Mem_GetRandomizedInit() { + return sIsInsideRandomizedInit; +} + +void N64Mem_SetRandomizedInit(int32_t value) { + sIsInsideRandomizedInit = value; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Actor overlays +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocOverlay(int16_t actorId, uint16_t allocType) { + if (!sIsActive) { + return 1; + } + + // Child actors spawned during a randomized enemy's init don't exist on N64 -- skip shadow tracking. + if (sIsInsideRandomizedInit) { + return 1; + } + + if (actorId < 0 || actorId >= ACTOR_ID_MAX) { + return 1; + } + + const uint32_t overlaySize = N64SizeData_GetActorOverlaySize(ResolveSizeActorId(actorId)); + if (overlaySize == 0) { + return 1; + } + + // When the enemy randomizer is active, use the original actor's allocType so the shadow takes the same path + // N64 would have taken for the unsubstituted actor (i.e., a regular enemy replaced by an ABSOLUTE mini-boss + // should still shadow as ALLOCTYPE_NORMAL). + if (sOriginalActorId >= 0) { + const auto& entry = ActorDB::Instance->RetrieveEntry(sOriginalActorId); + allocType = entry.entry.allocType; + } + + // ABSOLUTE: Shared fixed-size buffer, allocated once via MallocR. + if (allocType & ALLOCTYPE_ABSOLUTE) { + if (sAbsoluteSpaceShadow == SHADOW_NULL) { + sAbsoluteSpaceShadow = ShadowArena_MallocR(&sShadow, AM_FIELD_SIZE); + if (sAbsoluteSpaceShadow == SHADOW_NULL) { + SPDLOG_ERROR("[HardwareMemoryLimits] Shadow absolute space failed (need 0x{:X})", AM_FIELD_SIZE); + return 0; + } + sBlockMetaMap[sAbsoluteSpaceShadow] = { N64MEM_BLOCK_ABSOLUTE, -1 }; + } + + return 1; + } + + // Resolve the actor ID for overlay tracking. When the enemy randomizer is active, overlays are tracked by + // the ORIGINAL actor ID so that multiple replacements of the same original type share one overlay shadow. + const uint16_t overlayTrackId = ResolveSizeActorId(actorId); + + // Already shadowed for this type. + if (sOverlayShadows[overlayTrackId] != SHADOW_NULL) { + return 1; + } + + // Match Actor_LoadOverlay's branching: PERSISTENT/PERMANENT overlays go through MallocR (arena top), default + // (NORMAL) overlays go through forward Malloc (arena bottom). ABSOLUTE is handled above. NORMAL overlays being + // interleaved with instances at the bottom is the authentic N64 fragmentation pattern -- the simulation must + // reproduce it, not paper over it. + const uint32_t shadow = (allocType & ALLOCTYPE_PERMANENT) + ? ShadowArena_MallocR(&sShadow, overlaySize) + : ShadowArena_Malloc(&sShadow, overlaySize); + if (shadow == SHADOW_NULL) { + SPDLOG_ERROR("[HardwareMemoryLimits] Shadow overlay failed for actor 0x{:04X} (need 0x{:X})", actorId, + overlaySize); + return 0; + } + + sOverlayShadows[overlayTrackId] = shadow; + sBlockMetaMap[shadow] = { N64MEM_BLOCK_OVERLAY, static_cast(overlayTrackId) }; + TraceAlloc("ovl", overlayTrackId, overlaySize); + return 1; +} + +void N64Mem_FreeOverlay(int16_t actorId, uint16_t allocType) { + if (!sIsActive) { + return; + } + + if (actorId < 0 || actorId >= ACTOR_ID_MAX) { + return; + } + + // PERMANENT: Overlays that are never freed. + if (allocType & ALLOCTYPE_PERMANENT) { + return; + } + + sBlockMetaMap.erase(sOverlayShadows[actorId]); + ShadowArena_Free(&sShadow, sOverlayShadows[actorId]); + sOverlayShadows[actorId] = SHADOW_NULL; + TraceFree("ovl", actorId); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Actor instances +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocInstance(int16_t actorId, int16_t params, void* realPtr) { + if (!sIsActive) { + sOriginalActorId = -1; + return 1; + } + + // Child actors spawned during a randomized enemy's init don't exist on N64 -- skip shadow tracking. + if (sIsInsideRandomizedInit) { + return 1; + } + + const uint16_t sizeId = ResolveSizeActorId(actorId); + + // Consume the override and enter the randomized-init phase. Subsidiaries allocated during Actor_Init will be + // skipped (they belong to the replacement, not the original). Child Actor_Spawn calls during init will also + // be skipped via the sIsInsideRandomizedInit check above. + if (sOriginalActorId >= 0) { + sOriginalActorId = -1; + sIsInsideRandomizedInit = 1; + } + + if (actorId < 0 || actorId >= ACTOR_ID_MAX) { + return 1; + } + + const uint32_t instanceSize = N64SizeData_GetActorInstanceSize(sizeId); + if (instanceSize == 0) { + return 1; + } + + if (instanceSize == 0x1A0) { + SPDLOG_INFO("[HardwareMemoryLimits] 0x1A0 instance: actorId=0x{:X}", actorId); + } + + const uint32_t shadow = ShadowArena_Malloc(&sShadow, instanceSize); + if (shadow == SHADOW_NULL) { + SPDLOG_ERROR("[HardwareMemoryLimits] Shadow instance failed for actor 0x{:04X} params=0x{:04X} (need 0x{:X})", + actorId, static_cast(params), instanceSize); + N64Mem_DumpArena(fmt::format("instance failure: actor=0x{:04X} params=0x{:04X}", actorId, + static_cast(params)) + .c_str()); + return 0; + } + + sShadowMap[realPtr] = shadow; + sActorOriginalIds[realPtr] = static_cast(sizeId); + sBlockMetaMap[shadow] = { N64MEM_BLOCK_INSTANCE, static_cast(sizeId) }; + TraceAlloc("inst", actorId, instanceSize); + return 1; +} + +int16_t N64Mem_FreeInstance(void* realPtr) { + if (!sIsActive || !realPtr) { + return -1; + } + + const auto i = sShadowMap.find(realPtr); + if (i == sShadowMap.end()) { + return -1; + } + + // Retrieve the stored original actor ID before erasing. Used by Actor_Delete to free the correct overlay shadow. + int16_t originalId = -1; + if (const auto j = sActorOriginalIds.find(realPtr); j != sActorOriginalIds.end()) { + originalId = j->second; + sActorOriginalIds.erase(j); + } + + if (sTraceEnabled) { + const auto* actor = static_cast(realPtr); + TraceFree("inst", actor->id); + } + + sBlockMetaMap.erase(i->second); + ShadowArena_Free(&sShadow, i->second); + sShadowMap.erase(i); + return originalId; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Subsidiaries +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocSubsidiary(void* realPtr, uint32_t n64Size) { + if (!sIsActive) { + return 1; + } + + // When inside a randomized enemy's init, the replacement actor's subsidiaries (colliders, skeleton tables, skin + // buffers) have different counts than the original's. Rather than charge the wrong sizes, skip subsidiary + // tracking entirely -- instance and overlay sizes from the original are already correct and dominate heap pressure. + if (sIsInsideRandomizedInit) { + return 1; + } + + const uint32_t shadow = ShadowArena_Malloc(&sShadow, n64Size); + if (shadow == SHADOW_NULL) { + SPDLOG_ERROR("[HardwareMemoryLimits] Shadow subsidiary failed (need 0x{:X})", n64Size); + return 0; + } + + sShadowMap[realPtr] = shadow; + sBlockMetaMap[shadow] = { N64MEM_BLOCK_SUBSIDIARY, -1 }; + TraceAlloc("sub", 0, n64Size); + return 1; +} + +void N64Mem_FreeSubsidiary(void* realPtr) { + if (!sIsActive || !realPtr) { + return; + } + + const auto i = sShadowMap.find(realPtr); + if (i == sShadowMap.end()) { + return; + } + + sBlockMetaMap.erase(i->second); + ShadowArena_Free(&sShadow, i->second); + sShadowMap.erase(i); +} + + +// -------------------------------------------------------------------------------------------------------------------- +// Effect overlays +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocEffectOverlay(int32_t type) { + if (!sIsActive) { + return 1; + } + + // Effect overlays triggered by a randomized replacement's children or update don't exist on N64 -- skip shadowing. + if (sIsInsideRandomizedInit) { + return 1; + } + + if (type < 0 || type >= EFFECT_SS_TYPE_MAX) { + return 1; + } + + if (sEffectOverlayShadows[type] != SHADOW_NULL) { + return 1; + } + + uint32_t overlaySize = N64SizeData_GetEffectOverlaySize(type); + if (overlaySize == 0) { + return 1; + } + + const uint32_t shadow = ShadowArena_MallocR(&sShadow, overlaySize); + if (shadow == SHADOW_NULL) { + SPDLOG_ERROR("[HardwareMemoryLimits] Shadow effect overlay failed for type 0x{:02X} (need 0x{:X})", type, + overlaySize); + return 0; + } + + sEffectOverlayShadows[type] = shadow; + sBlockMetaMap[shadow] = { N64MEM_BLOCK_EFFECT, static_cast(type) }; + TraceAlloc("efx", type, overlaySize); + return 1; +} + + +// -------------------------------------------------------------------------------------------------------------------- +// Heap viewer metadata +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_GetBlockInfo(uint32_t dataOffset, uint8_t* outType, int16_t* outActorId) { + const auto i = sBlockMetaMap.find(dataOffset); + if (i == sBlockMetaMap.end()) { + return 0; + } + + if (outType) { + *outType = i->second.type; + } + + if (outActorId) { + *outActorId = i->second.actorId; + } + + return 1; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Registration +// -------------------------------------------------------------------------------------------------------------------- + +void RegisterN64MemoryModel() { + // #TODO: Shadow arena initialization +} + +static RegisterShipInitFunc initFunc(RegisterN64MemoryModel, { CVAR_NAME }); \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp new file mode 100644 index 00000000000..85c3e1a84ed --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// N64 subsidiary struct sizes (32-bit, from decomp headers and linker map). +#define N64_SIZEOF_COLLIDER_JNT_SPH_ELEM 0x40 +#define N64_SIZEOF_COLLIDER_TRIS_ELEM 0x5C +#define N64_SIZEOF_CAMERA 0x16C +#define N64_SIZEOF_SKIN_LIMB_VTX 0x0C + +// -------------------------------------------------------------------------------------------------------------------- +// Lifecycle +// -------------------------------------------------------------------------------------------------------------------- + +// Store SoH's THA remainder before ZeldaArena_Init consumes it. Call from Play_Init immediately after +// THA_GetRemaining. +void N64Mem_StoreThaRemainder(uint32_t sohRemainder); + +// Store which elf message file was loaded by Scene_CommandSpecialFiles (1 = elf_message_field, 2 = elf_message_ydan, +// 0 = none). Called from z_scene.c during scene command processing. +void N64Mem_StoreElfMsgNum(uint8_t num); + +// Returns the elf message number stored by N64Mem_StoreElfMsgNum. +uint8_t N64Mem_GetElfMsgNum(void); + +// Reset shadow state, compute N64-equivalent arena size from stored THA remainder minus N64-specific consumers +// (room buffers, etc.), and reread CVar. Call from Play_Init after ZeldaArena_Init. +struct PlayState; +void N64Mem_Reset(PlayState* play); + +// Returns whether the N64 memory model is currently active. +int32_t N64Mem_IsActive(void); + +// Returns the shadow arena pointer for debug visualization. Only valid when N64Mem_IsActive() is true. +struct ShadowArena* N64Mem_GetShadowArena(void); + +// Log the current shadow arena state (alloc/free/largest) with the given context label. +void N64Mem_LogState(const char* context); + +// -------------------------------------------------------------------------------------------------------------------- +// Enemy randomizer compatibility +// +// When the enemy randomizer replaces an actor, the shadow arena should charge the ORIGINAL actor's N64 sizes (overlay +// and instance) rather than the replacement's. This preserves authentic N64 heap geometry -- memory-dependent +// behaviors (SRM, ACE, spawn failure thresholds) remain consistent regardless of which enemies are on screen. +// +// Call SetOriginalActorId with the scene's original actor ID before Actor_Spawn, and ClearOriginalActorId after +// Actor_Spawn returns. When set, overlay and instance size lookups use the original ID; all other tracking (overlay +// ref counting, instance pointer mapping) uses the actual spawned actor ID. +// -------------------------------------------------------------------------------------------------------------------- + +void N64Mem_SetOriginalActorId(int16_t actorId); +void N64Mem_ClearOriginalActorId(void); + +// Returns true if the actor at realPtr is a randomized replacement (i.e., has an original-ID entry), false otherwise. +// Used by Actor_UpdateAll to suppress shadow allocations for children spawned during a replacement's update. +int32_t N64Mem_IsRandomizedActor(void* realPtr); + +// Save/restore for the randomized-init skip flag. Actor_Spawn saves the current value at entry and restores it +// after Actor_Init returns, so child spawns during a randomized actor's init don't clobber the parent's flag. +int32_t N64Mem_GetRandomizedInit(void); +void N64Mem_SetRandomizedInit(int32_t value); + +// Log Graveyard benchmark data (transition count, largest_free, total_free). Call after room actors are spawned. +void N64Mem_BenchmarkTransition(PlayState* play); + +// -------------------------------------------------------------------------------------------------------------------- +// Actor overlays: Keyed by actor ID, shadow-only allocations. +// +// On N64, overlays load into ZeldaArena from ROM on first spawn and free when no instances remain. SoH compiles them +// into the binary, so they never touch ZeldaArena. The shadow restores this pressure. +// +// Call AllocOverlay when numLoaded transitions 0 -> true. +// Call FreeOverlay when numLoaded transitions 1 -> false. +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocOverlay(int16_t actorId, uint16_t allocType); +void N64Mem_FreeOverlay(int16_t actorId, uint16_t allocType); + +// -------------------------------------------------------------------------------------------------------------------- +// Actor instances: Paired with real ZeldaArena allocations. +// +// Call AllocInstance after the real allocation succeeds. If the shadow cannot satisfy the N64-sized allocation, +// returns false and the caller should treat the spawn as failed. +// +// Call FreeInstance when the actor is deleted. +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocInstance(int16_t actorId, int16_t params, void* realPtr); +// Returns the stored original actor ID for overlay free-path tracking, or -1 if not tracked. +int16_t N64Mem_FreeInstance(void* realPtr); + +// -------------------------------------------------------------------------------------------------------------------- +// Subsidiaries (colliders, camera, skin, etc.): Paired. +// +// Same pattern as instances -- shadow-alloc at N64 size, gate on failure, free when the real allocation is freed. +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocSubsidiary(void* realPtr, uint32_t n64Size); +void N64Mem_FreeSubsidiary(void* realPtr); + +// -------------------------------------------------------------------------------------------------------------------- +// Effect overlays: Shadow-only, persist for scene lifetime. +// +// On N64, effect overlays load via MallocR on first spawn and are never freed until the GameState is torn down. +// -------------------------------------------------------------------------------------------------------------------- + +int32_t N64Mem_AllocEffectOverlay(int32_t type); + +// -------------------------------------------------------------------------------------------------------------------- +// Heap viewer metadata +// +// Query block identity from the shadow arena. Returns true if metadata exists for the given data offset, false if +// not. Block type constants identify the allocation category; actorId is the original (pre-randomizer) actor ID for +// instance/overlay blocks, or -1 for subsidiary/effect/absolute blocks. +// -------------------------------------------------------------------------------------------------------------------- + +#define N64MEM_BLOCK_FREE 0 +#define N64MEM_BLOCK_INSTANCE 1 +#define N64MEM_BLOCK_OVERLAY 2 +#define N64MEM_BLOCK_SUBSIDIARY 3 +#define N64MEM_BLOCK_EFFECT 4 +#define N64MEM_BLOCK_ABSOLUTE 5 + +int32_t N64Mem_GetBlockInfo(uint32_t dataOffset, uint8_t* outType, int16_t* outActorId); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.cpp b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.cpp new file mode 100644 index 00000000000..6e1b8762dcc --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.cpp @@ -0,0 +1,277 @@ +#include "N64SizeData.hpp" + +#include + +// -------------------------------------------------------------------------------------------------------------------- +// DMA file sizes (misc/n64_memory/dma_sizes) +// +// Format: uint32_t entryCount, then per entry: uint32_t vromSize, length-prefixed string name +// -------------------------------------------------------------------------------------------------------------------- + +static std::unordered_map sDmaFileSizes; +static bool sIsDmaLoaded = false; + +static void LoadDmaFileSizes() { + if (sIsDmaLoaded) { + return; + } + + sIsDmaLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile("misc/n64_memory/dma_sizes"); + if (!file || !file->IsLoaded) { + SPDLOG_ERROR("[N64SizeData] Failed to load misc/n64_memory/dma_sizes from OTR."); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + const uint32_t entryCount = reader->ReadUInt32(); + for (std::size_t i = 0; i < entryCount; ++i) { + const uint32_t vromSize = reader->ReadUInt32(); + const std::string name = reader->ReadString(); + sDmaFileSizes[name] = vromSize; + } + + SPDLOG_INFO("[N64SizeData] Loaded {} DMA file sizes from OTR.", sDmaFileSizes.size()); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Actor overlay VRAM sizes (misc/n64_memory/actor_overlay_sizes) +// +// Format: uint32_t entryCount, then entryCount consecutive uint32_t values indexed by actor ID +// -------------------------------------------------------------------------------------------------------------------- + +static std::vector sActorOverlaySizes; +static bool sIsActorOverlayLoaded = false; + +static void LoadActorOverlaySizes() { + if (sIsActorOverlayLoaded) { + return; + } + + sIsActorOverlayLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile( + "misc/n64_memory/actor_overlay_sizes"); + if (!file || !file->IsLoaded) { + SPDLOG_ERROR("[N64SizeData] Failed to load misc/n64_memory/actor_overlay_sizes from OTR."); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + const uint32_t entryCount = reader->ReadUInt32(); + sActorOverlaySizes.resize(entryCount); + + for (std::size_t i = 0; i < entryCount; ++i) { + sActorOverlaySizes.at(i) = reader->ReadUInt32(); + } + + SPDLOG_INFO("[N64SizeData] Loaded {} actor overlay sizes.", sActorOverlaySizes.size()); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Effect overlay VRAM sizes (misc/n64_memory/effect_overlay_sizes) +// +// Format: uint32_t entryCount, then entryCount consecutive uint32_t values indexed by effect type +// -------------------------------------------------------------------------------------------------------------------- + +static std::vector sEffectOverlaySizes; +static bool sIsEffectOverlayLoaded = false; + +static void LoadEffectOverlaySizes() { + if (sIsEffectOverlayLoaded) { + return; + } + + sIsEffectOverlayLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile( + "misc/n64_memory/effect_overlay_sizes"); + if (!file || !file->IsLoaded) { + SPDLOG_ERROR("[N64SizeData] Failed to load misc/n64_memory/effect_overlay_sizes from OTR."); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + const uint32_t entryCount = reader->ReadUInt32(); + sEffectOverlaySizes.resize(entryCount); + + for (std::size_t i = 0; i < entryCount; ++i) { + sEffectOverlaySizes.at(i) = reader->ReadUInt32(); + } + + SPDLOG_INFO("[N64SizeData] Loaded {} effect overlay sizes.", sEffectOverlaySizes.size()); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Actor instance sizes (misc/n64_memory/actor_instance_sizes) +// +// Format: uint32_t entryCount, then entryCount consecutive uint32_t values indexed by actor ID +// Each value is the N64 sizeof the actor's instance struct, read from ActorProfile.instanceSize. +// -------------------------------------------------------------------------------------------------------------------- + +static std::vector sActorInstanceSizes; +static bool sIsActorInstanceLoaded = false; + +static void LoadActorInstanceSizes() { + if (sIsActorInstanceLoaded) { + return; + } + + sIsActorInstanceLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile( + "misc/n64_memory/actor_instance_sizes"); + if (!file || !file->IsLoaded) { + SPDLOG_ERROR("[N64SizeData] Failed to load misc/n64_memory/actor_instance_sizes from OTR."); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + const uint32_t entryCount = reader->ReadUInt32(); + sActorInstanceSizes.resize(entryCount); + + for (std::size_t i = 0; i < entryCount; ++i) { + sActorInstanceSizes.at(i) = reader->ReadUInt32(); + } + + SPDLOG_INFO("[N64SizeData] Loaded {} actor instance sizes.", sActorInstanceSizes.size()); +} + +// -------------------------------------------------------------------------------------------------------------------- +// API +// -------------------------------------------------------------------------------------------------------------------- + +extern "C" uint32_t N64SizeData_GetDmaFileSize(const char* name) { + LoadDmaFileSizes(); + + if (const auto i = sDmaFileSizes.find(name); i != sDmaFileSizes.end()) { + return i->second; + } + + SPDLOG_WARN("[N64SizeData] DMA file size not found for '{}'", name); + return 0; +} + +extern "C" uint32_t N64SizeData_GetActorOverlaySize(uint16_t actorId) { + LoadActorOverlaySizes(); + + if (actorId < sActorOverlaySizes.size()) { + return sActorOverlaySizes.at(actorId); + } + + return 0; +} + +extern "C" uint32_t N64SizeData_GetEffectOverlaySize(uint16_t effectType) { + LoadEffectOverlaySizes(); + + if (effectType < sEffectOverlaySizes.size()) { + return sEffectOverlaySizes.at(effectType); + } + + return 0; +} + +extern "C" uint32_t N64SizeData_GetActorInstanceSize(uint16_t actorId) { + LoadActorInstanceSizes(); + + if (actorId < sActorInstanceSizes.size()) { + return sActorInstanceSizes.at(actorId); + } + + return 0; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Kaleido overlay max VRAM size (misc/n64_memory/kaleido_vram_size) +// +// Format: single uint32_t -- max(ovl_kaleido_scope VRAM, ovl_player_actor VRAM) +// -------------------------------------------------------------------------------------------------------------------- + +static uint32_t sKaleidoVramSize = 0; +static bool sIsKaleidoLoaded = false; + +static void LoadKaleidoVramSize() { + if (sIsKaleidoLoaded) { + return; + } + + sIsKaleidoLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile( + "misc/n64_memory/kaleido_vram_size"); + if (!file || !file->IsLoaded) { + SPDLOG_ERROR("[N64SizeData] Failed to load misc/n64_memory/kaleido_vram_size from OTR."); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + sKaleidoVramSize = reader->ReadUInt32(); + + SPDLOG_INFO("[N64SizeData] Kaleido max VRAM size: 0x{:X}.", sKaleidoVramSize); +} + +extern "C" uint32_t N64SizeData_GetKaleidoVramSize() { + LoadKaleidoVramSize(); + return sKaleidoVramSize; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Arena node size +// +// Format: single uint32_t -- ArenaNode size for this ROM version (0x10 retail, 0x30 debug) +// -------------------------------------------------------------------------------------------------------------------- + +static uint32_t sArenaNodeSize = 0x30; // Default to debug (safe fallback -- over-estimates node overhead) +static bool sIsArenaNodeSizeLoaded = false; + +static void LoadArenaNodeSize() { + if (sIsArenaNodeSizeLoaded) { + return; + } + + sIsArenaNodeSizeLoaded = true; + + const auto file = + Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile( + "misc/n64_memory/arena_node_size"); + if (!file || !file->IsLoaded) { + SPDLOG_WARN("[N64SizeData] Failed to load misc/n64_memory/arena_node_size from OTR, defaulting to 0x{:X}.", + sArenaNodeSize); + return; + } + + auto stream = std::make_shared(file->Buffer->data(), file->Buffer->size()); + const auto reader = std::make_shared(stream); + reader->SetEndianness(Ship::Endianness::Big); + + sArenaNodeSize = reader->ReadUInt32(); + + SPDLOG_INFO("[N64SizeData] Arena node size: 0x{:X}.", sArenaNodeSize); +} + +extern "C" uint32_t N64SizeData_GetArenaNodeSize() { + LoadArenaNodeSize(); + return sArenaNodeSize; +} \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.hpp b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.hpp new file mode 100644 index 00000000000..15de0488ad9 --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// -------------------------------------------------------------------------------------------------------------------- +// N64 size data loaded from the OTR archive. +// +// All data is extracted per-version by OTRExporter during ROM extraction: +// misc/n64_memory/dma_sizes DMA file VROM sizes keyed by filename +// misc/n64_memory/actor_overlay_sizes Actor overlay VRAM sizes indexed by actor ID +// misc/n64_memory/effect_overlay_sizes Effect overlay VRAM sizes indexed by effect type +// misc/n64_memory/actor_instance_sizes Actor instance struct sizes indexed by actor ID +// misc/n64_memory/kaleido_vram_size max(ovl_kaleido_scope VRAM, ovl_player_actor VRAM) +// misc/n64_memory/arena_node_size N64 ArenaNode size (0x10 retail, 0x30 debug) +// +// Each table is loaded lazily on first query and cached for the lifetime of the process. +// -------------------------------------------------------------------------------------------------------------------- + +uint32_t N64SizeData_GetDmaFileSize(const char* name); +uint32_t N64SizeData_GetActorOverlaySize(uint16_t actorId); +uint32_t N64SizeData_GetEffectOverlaySize(uint16_t effectType); +uint32_t N64SizeData_GetActorInstanceSize(uint16_t actorId); +uint32_t N64SizeData_GetKaleidoVramSize(void); +uint32_t N64SizeData_GetArenaNodeSize(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.c b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.c new file mode 100644 index 00000000000..436a868fabc --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.c @@ -0,0 +1,414 @@ +#include "n64_arena_sizing.h" +#include "N64SizeData.hpp" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" + +#include + +#include "macros.h" +#include "variables.h" + +// Declared in z_bgcheck.c but not exposed via header. +s32 BgCheck_IsSpotScene(PlayState* play); +s32 BgCheck_TryGetCustomMemsize(s32 sceneId, uint32_t* memSize); + +// -------------------------------------------------------------------------------------------------------------------- +// N64 THA budget +// +// GameState_Realloc is called with this value in Play_Init when the memory model CVar is active. This is the total +// pool from which every THA consumer draws; whatever remains becomes ZeldaArena. +// -------------------------------------------------------------------------------------------------------------------- + +#define N64_THA_BUDGET 0x1D4790 + +// -------------------------------------------------------------------------------------------------------------------- +// N64 struct sizes (32-bit, from decomp headers and linker map) +// -------------------------------------------------------------------------------------------------------------------- + +#define N64_SIZEOF_COLLISION_CONTEXT 0x1464 // CollisionContext size = 0x1464 (from decomp header comment) +#define N64_SIZEOF_GFX 8 // sizeof(Gfx) on N64: Two uint32_t words -- SoH is 16 +#define N64_SIZEOF_VTX 0x10 // sizeof(Vtx) on N64: Same on both platforms +#define N64_SIZEOF_EFFECT_SS 0x60 // sizeof(EffectSs) on N64: No pointer members, constant across all N64 versions + + + +// Struct sizes that are identical on N64 and SoH (no pointer members): +// sizeof(MtxF) = 0x40 +// sizeof(GFx) = 0x08 +// sizeof(Vtx) = 0x10 +// sizeof(Vec3s) = 0x06 +// sizeof(SSNode) = 0x04 +// sizeof(CollisionPoly) = 0x10 +// sizeof(StaticLookup) = 0x06 + +// -------------------------------------------------------------------------------------------------------------------- +// Fixed THA consumers (same value on every N64 scene) +// -------------------------------------------------------------------------------------------------------------------- + +#define N64_MATRIX_STACK_SIZE (20 * 0x40) // sys_matrix.c: 20 * sizeof(MtxF) +#define N64_TEXT_BOX_SIZE 0x2200 // Message_Init: Constant +#define N64_DO_ACTION_SIZE 0x480 // z_construct.c: 3 * DO_ACTION_TEX_SIZE (48x16 IA4 = 0x180 each) +#define N64_ICON_ITEM_SIZE (0x1000 * 4) // z_construct.c: 4 * ITEM_ICON_SIZE (32x32 RGBA32) +#define N64_MAP_SEGMENT_SIZE 0x1000 // z_map_exp.c: DMA target buffer for minimap textures + +// -------------------------------------------------------------------------------------------------------------------- +// ovl_map_mark_data: N64-unique THA consumer for dungeon map marks +// +// On N64, the map mark data overlay (chest/boss/dungeon icons on the pause map) is loaded into THA +// via GAME_STATE_ALLOC in MapMark_Init. SoH compiles this data in directly. +// VRAM size from decomp linker map: 0x8085D460 - 0x80856900 = 0x6B60. Constant across OoT versions (dungeon map mark +// positions don't change). Only loaded for the 10 main dungeons (Deku Tree through Ice Cavern) and their boss rooms. +// -------------------------------------------------------------------------------------------------------------------- + +#define N64_MAP_MARK_DATA_VRAM_SIZE 0x6B60 + +static uint32_t GetMapMarkDataOverlaySize(PlayState* play) { + // Mirrors the condition in z_map_exp.c Map_Init: the dungeon case block's inner guard. + // Main dungeons: SCENE_DEKU_TREE (0x00) through SCENE_ICE_CAVERN (0x09) + // Boss rooms: SCENE_DEKU_TREE_BOSS (0x11) through SCENE_SHADOW_TEMPLE_BOSS (0x18) + if (play->sceneNum <= SCENE_ICE_CAVERN || + (play->sceneNum >= SCENE_DEKU_TREE_BOSS && play->sceneNum <= SCENE_SHADOW_TEMPLE_BOSS)) { + return N64_MAP_MARK_DATA_VRAM_SIZE; + } + + return 0; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Skybox N64-unique THA consumers +// +// On N64, skybox textures and palettes are DMA'd into THA-allocated staticSegment buffers. SoH loads from OTR via the +// ResourceManager, never touching THA. +// +// The allocation pattern depends on the skybox type: +// SKYBOX_NORMAL_SKY / OVERCAST_SUNSET: 2 texture banks + 2 palettes (all banks are 0xC000/0x100) +// SKYBOX_CUTSCENE_MAP: 2 different tex files + 2 palettes +// Indoor skyboxes: 1 tex file + 1 palette file (sizes vary per skybox) +// SKYBOX_NONE: Nothing +// +// The dList/vtx buffer pattern depends on drawType: +// SKYBOX_DRAW_128 (outdoor): dList=12×150×Gfx, vtx=5×32×Vtx (6×32 for CUTSCENE_MAP) +// SKYBOX_DRAW_256 (indoor, 3- or 4-face): dList=8×150×Gfx, vtx=8×32×Vtx +// -------------------------------------------------------------------------------------------------------------------- + +// DMA file name for a single-file indoor skybox (1 tex + 1 pal). +typedef struct { + const char* texName; + const char* palName; +} SkyboxDmaEntry; + +// Indexed by skybox ID. NULL texName means the ID isn't a single-file indoor skybox (handled separately). +static const SkyboxDmaEntry sSkyboxDmaTable[] = { + [SKYBOX_NONE] = { NULL, NULL }, + [SKYBOX_NORMAL_SKY] = { NULL, NULL }, // gNormalSkyFiles, hardcoded + [SKYBOX_BAZAAR] = { "vr_SP1a_static", "vr_SP1a_pal_static" }, + [SKYBOX_OVERCAST_SUNSET] = { NULL, NULL }, // same as NORMAL_SKY + [SKYBOX_MARKET_ADULT] = { "vr_RUVR_static", "vr_RUVR_pal_static" }, + [SKYBOX_CUTSCENE_MAP] = { NULL, NULL }, // Two tex files, handled separately + [SKYBOX_HOUSE_LINK] = { "vr_LHVR_static", "vr_LHVR_pal_static" }, + [SKYBOX_MARKET_CHILD_DAY] = { "vr_MDVR_static", "vr_MDVR_pal_static" }, + [SKYBOX_MARKET_CHILD_NIGHT] = { "vr_MNVR_static", "vr_MNVR_pal_static" }, + [SKYBOX_HAPPY_MASK_SHOP] = { "vr_FCVR_static", "vr_FCVR_pal_static" }, + [SKYBOX_HOUSE_KNOW_IT_ALL_BROTHERS] = { "vr_KHVR_static", "vr_KHVR_pal_static" }, + [SKYBOX_HOUSE_OF_TWINS] = { "vr_K3VR_static", "vr_K3VR_pal_static" }, + [SKYBOX_STABLES] = { "vr_MLVR_static", "vr_MLVR_pal_static" }, + [SKYBOX_HOUSE_KAKARIKO] = { "vr_KKRVR_static", "vr_KKRVR_pal_static" }, + [SKYBOX_KOKIRI_SHOP] = { "vr_KSVR_static", "vr_KSVR_pal_static" }, + [SKYBOX_GORON_SHOP] = { "vr_GLVR_static", "vr_GLVR_pal_static" }, + [SKYBOX_ZORA_SHOP] = { "vr_ZRVR_static", "vr_ZRVR_pal_static" }, + [SKYBOX_POTION_SHOP_KAKARIKO] = { "vr_DGVR_static", "vr_DGVR_pal_static" }, + [SKYBOX_POTION_SHOP_MARKET] = { "vr_ALVR_static", "vr_ALVR_pal_static" }, + [SKYBOX_BOMBCHU_SHOP] = { "vr_NSVR_static", "vr_NSVR_pal_static" }, + [SKYBOX_HOUSE_RICHARD] = { "vr_IPVR_static", "vr_IPVR_pal_static" }, + [SKYBOX_HOUSE_IMPA] = { "vr_LBVR_static", "vr_LBVR_pal_static" }, + [SKYBOX_TENT] = { "vr_TTVR_static", "vr_TTVR_pal_static" }, + [SKYBOX_HOUSE_MIDO] = { "vr_K4VR_static", "vr_K4VR_pal_static" }, + [SKYBOX_HOUSE_SARIA] = { "vr_K5VR_static", "vr_K5VR_pal_static" }, + [SKYBOX_HOUSE_ALLEY] = { "vr_KR3VR_static", "vr_KR3VR_pal_static" }, +}; + +static uint32_t GetN64SkyboxTextureSize(int16_t skyboxId) { + if (skyboxId == SKYBOX_NONE) { + return 0; + } + + // NORMAL_SKY and OVERCAST_SUNSET both load 2 texture banks + 2 palettes from the vr_fine/vr_cloud files. + // All 16 banks are exactly 0xC000 and all 16 palettes are exactly 0x100, so the total is constant. + if (skyboxId == SKYBOX_NORMAL_SKY || skyboxId == SKYBOX_OVERCAST_SUNSET) { + return 2 * 0xC000 + 2 * 0x100; + } + + // CUTSCENE_MAP loads two different texture files + 2 palette copies. + if (skyboxId == SKYBOX_CUTSCENE_MAP) { + const uint32_t tex0 = N64SizeData_GetDmaFileSize("vr_holy0_static"); + const uint32_t tex1 = N64SizeData_GetDmaFileSize("vr_holy1_static"); + const uint32_t pal = N64SizeData_GetDmaFileSize("vr_holy0_pal_static"); + return tex0 + tex1 + pal * 2; + } + + // Indoor skyboxes: 1 texture + 1 palette, looked up from the DMA blob. + if (skyboxId >= 0 && skyboxId < (int16_t)ARRAY_COUNT(sSkyboxDmaTable)) { + const SkyboxDmaEntry* entry = &sSkyboxDmaTable[skyboxId]; + if (entry->texName != NULL) { + const uint32_t tex = N64SizeData_GetDmaFileSize(entry->texName); + const uint32_t pal = N64SizeData_GetDmaFileSize(entry->palName); + return tex + pal; + } + } + + return 0; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Skybox dListBuf and roomVtx +// +// Allocation sizes depend on the drawType, which is determined by the skybox ID: +// SKYBOX_DRAW_128 (NORMAL_SKY, OVERCAST_SUNSET, CUTSCENE_MAP): 12-face dList, 5- or 6-face vtx +// SKYBOX_DRAW_256 (all indoor skyboxes): 8-face dList, 8-face vtx + +// Gfx is 8 bytes on N64, Vtx is 0x10. +// -------------------------------------------------------------------------------------------------------------------- + +static void GetSkyboxDlistAndVtxSize(int16_t skyboxId, uint32_t* outDlistSize, uint32_t* outVtxSize) { + if (skyboxId == SKYBOX_NONE) { + *outDlistSize = 0; + *outVtxSize = 0; + return; + } + + if (skyboxId == SKYBOX_NORMAL_SKY || skyboxId == SKYBOX_OVERCAST_SUNSET) { + *outDlistSize = 12 * 150 * N64_SIZEOF_GFX; + *outVtxSize = 5 * 32 * N64_SIZEOF_VTX; + } else if (skyboxId == SKYBOX_CUTSCENE_MAP) { + *outDlistSize = 12 * 150 * N64_SIZEOF_GFX; + *outVtxSize = 6 * 32 * N64_SIZEOF_VTX; + } else { + // Indoor skyboxes: SKYBOX_DRAW_256_4FACE or SKYBOX_DRAW_256_3FACE, both use the same allocation. + *outDlistSize = 8 * 150 * N64_SIZEOF_GFX; + *outVtxSize = 8 * 32 * N64_SIZEOF_VTX; + } +} + +// -------------------------------------------------------------------------------------------------------------------- +// BgCheck THA Total +// +// The BgCheck system allocates 6 items from THA: lookupTbl, SSNode tbl, polyCheckTbl, dyna polyList, dyna vtxList, +// dyna polyNodes. +// +// By algebra, the total simplifies to: bgcheck_memSize - sizeof(CollisionContext) +// +// This is because the tblMax formula (z_bgcheck.c:1628) is defined as: +// tblMax = (memSize - overhead) / sizeof(SSNode) +// +// where overhead includes all the other consumers. When you sum all 6 THA allocations, every term cancels except +// bgcheck_memSize and sizeof(CollisionContext). +// -------------------------------------------------------------------------------------------------------------------- + +static uint32_t GetBgCheckMemSize(PlayState* play) { + const int16_t sceneNum = play->sceneNum; + + if (YREG(15) == 0x10 || YREG(15) == 0x20 || YREG(15) == 0x30 || YREG(15) == 0x40) { + return sceneNum == SCENE_STABLE ? 0x3520 : 0x4E20; + } + + if (BgCheck_IsSpotScene(play)) { + return 0xF000; + } + + uint32_t customMemSize = 0; + if (BgCheck_TryGetCustomMemsize(sceneNum, &customMemSize)) { + return customMemSize; + } + + return 0x1CC00; +} + +static uint32_t GetBgCheckThaTotal(PlayState* play) { + return GetBgCheckMemSize(play) - N64_SIZEOF_COLLISION_CONTEXT; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Object bank size (mirrors z_scene.c Object_InitBlank) +// -------------------------------------------------------------------------------------------------------------------- + +static uint32_t GetObjectBankSize(PlayState* play) { + const int16_t sceneNum = play->sceneNum; + if (sceneNum == SCENE_GANON_BOSS && gSaveContext.sceneSetupIndex == 4) { + return 1177600; + } + + if (sceneNum == SCENE_SPIRIT_TEMPLE_BOSS || sceneNum == SCENE_CHAMBER_OF_THE_SAGES || + sceneNum == SCENE_GANONDORF_BOSS) { + return 1075200; + } + + return 1024000; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Elf message size +// +// On N64, loaded via Play_LoadFile into THA. SoH loads from OTR via ResourceManager. Only present if the scene's +// SpecialFiles command has cUpElfMsgNum != 0. The file name is determined by cUpElfMsgNum (1-indexed). +// -------------------------------------------------------------------------------------------------------------------- + +static const char* sElfMsgDmaNames[] = { + "elf_message_field", + "elf_message_ydan", +}; + +static uint32_t GetElfMessageSize(PlayState* play) { + if (play->cUpElfMsgs == NULL) { + return 0; + } + + const uint8_t elfMsgNum = N64Mem_GetElfMsgNum(); + if (elfMsgNum == 0 || elfMsgNum > ARRAY_COUNT(sElfMsgDmaNames)) { + LUSLOG_WARN("[ArenaSizing] elfMsg: cUpElfMsgs non-NULL but elfMsgNum=%d out of range", elfMsgNum); + return 0; + } + + return N64SizeData_GetDmaFileSize(sElfMsgDmaNames[elfMsgNum - 1]); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Room buffer max size (mirrors func_80096FE8 logic) +// -------------------------------------------------------------------------------------------------------------------- + +static uintptr_t GetMaxRoomSize(PlayState* play) { + uintptr_t maxRoomSize = 0; + + for (size_t i = 0; i < play->numRooms; ++i) { + const uintptr_t roomSize = play->roomList[i].vromEnd - play->roomList[i].vromStart; + if (roomSize > maxRoomSize) { + maxRoomSize = roomSize; + } + } + + if (play->transiActorCtx.numActors != 0) { + const TransitionActorEntry* transitionActor = &play->transiActorCtx.list[0]; + + for (size_t j = 0; j < play->transiActorCtx.numActors; ++j) { + const int8_t frontRoom = transitionActor->sides[0].room; + const int8_t backRoom = transitionActor->sides[1].room; + const uintptr_t frontSize = frontRoom < 0 + ? 0 + : play->roomList[frontRoom].vromEnd - play->roomList[frontRoom].vromStart; + const uintptr_t backSize = backRoom < 0 + ? 0 + : play->roomList[backRoom].vromEnd - play->roomList[backRoom].vromStart; + + const uintptr_t cumulSize = frontRoom != backRoom ? frontSize + backSize : frontSize; + if (cumulSize > maxRoomSize) { + maxRoomSize = cumulSize; + } + + transitionActor++; + } + } + + return maxRoomSize; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Main computation +// -------------------------------------------------------------------------------------------------------------------- + +uint32_t ArenaSizing_ComputeN64ArenaSize(PlayState* play) { + // Each THA consumer is allocated via GAME_STATE_ALLOC -> THA_AllocTailAlign16, which consumes ALIGN16(size) bytes. + // We must align each consumer individually before summing; aligning the sum would under-count when individual + // sizes are not 16-byte aligned (common for DMA file sizes). BgCheck is the exception -- its internal allocations + // use mixed alignment, but the tblMax computation absorbs the internal waste, so the simplified total + // (memSize - sizeof(CollisionContext)) is used as-is. + + uint32_t total = 0; + + // Kaleido overlay buffer: max(ovl_kaleido_scope, ovl_player_actor) VRAM span, from OTR blob. + const uint32_t kaleidoVramSize = N64SizeData_GetKaleidoVramSize(); + total += ALIGN16(kaleidoVramSize); + + // parameter_static: DMA file size, version-specific but available in the OTR blob. + const uint32_t parameterStaticSize = N64SizeData_GetDmaFileSize("parameter_static"); + total += ALIGN16(parameterStaticSize); + + // Fixed consumers + { + total += ALIGN16(N64_MATRIX_STACK_SIZE); + total += ALIGN16(0x55 * N64_SIZEOF_EFFECT_SS); + total += ALIGN16(N64_TEXT_BOX_SIZE); + total += ALIGN16(N64_DO_ACTION_SIZE); + total += ALIGN16(N64_ICON_ITEM_SIZE); + total += ALIGN16(N64_MAP_SEGMENT_SIZE); + + const uint32_t fixed = total; + LUSLOG_INFO("[ArenaSizing] fixed=0x%X (kaleido=0x%X, param=0x%X)", fixed, kaleidoVramSize, + parameterStaticSize); + } + + // Scene-dependent consumers + { + const uint32_t objBank = GetObjectBankSize(play); + const uint32_t sceneFile = N64SizeData_GetDmaFileSize(play->loadedScene->sceneFile.fileName); + const uintptr_t roomBuf = GetMaxRoomSize(play); + total += ALIGN16(objBank); + total += ALIGN16(sceneFile); + total += ALIGN16(roomBuf); + LUSLOG_INFO("[ArenaSizing] objBank=0x%X, sceneFile=0x%X, roomBuf=0x%x", (uint32_t)objBank, (uint32_t)sceneFile, + (uint32_t)roomBuf); + } + + // ---------------------------------------------------------------------------------------------------------------- + // Skybox + // + // On N64 these are 3-5 separate GAME_STATE_ALLOC calls (tex0, tex1, palette, dList, vtx), each independently + // aligned. GetN64SkyboxTextureSize returns the combined raw size of the texture + palette allocations. + // For NORMAL_SKY this is 2×0xC000 + 2×0x100, all already 16-aligned, so ALIGN16 is a no-op here. The dList and + // vtx are separate allocations. + // ---------------------------------------------------------------------------------------------------------------- + { + uint32_t dListSize = 0; + uint32_t vtxSize = 0; + uint32_t texSize = 0; + GetSkyboxDlistAndVtxSize(play->skyboxId, &dListSize, &vtxSize); + texSize = GetN64SkyboxTextureSize(play->skyboxId); + total += ALIGN16(dListSize); + total += ALIGN16(vtxSize); + total += ALIGN16(texSize); + LUSLOG_INFO("[ArenaSizing] skyboxId=%d, dList=0x%X, vtx=0x%X, tex=0x%X", play->skyboxId, dListSize, vtxSize, + texSize); + } + + // ---------------------------------------------------------------------------------------------------------------- + // BgCheck + // + // Uses the algebraic simplification (memSize - sizeof(CollisionContext)). BgCheck's internal allocations use + // THA_AllocTailAlign with 2-byte alignment, and the tblMax formula absorbs internal alignment waste. No separate + // ALIGN16 needed here. + // ---------------------------------------------------------------------------------------------------------------- + { + const uint32_t bgCheck = GetBgCheckThaTotal(play); + total += bgCheck; + LUSLOG_INFO("[ArenaSizing] bgCheck=0x%X (memSize=0x%X)", bgCheck, GetBgCheckMemSize(play)); + } + + // Elf message + { + const uint32_t elfMsg = GetElfMessageSize(play); + total += ALIGN16(elfMsg); + LUSLOG_INFO("[ArenaSizing] elfMsg=0x%X", elfMsg); + } + + // Map mark data overlay (dungeons only) + { + const uint32_t mapMarkData = GetMapMarkDataOverlaySize(play); + total += ALIGN16(mapMarkData); + } + + LUSLOG_INFO("[ArenaSizing] total=0x%X, arena=0x%X (budget=0x%X)", total, N64_THA_BUDGET - total, + (uint32_t)N64_THA_BUDGET); + + if (total >= N64_THA_BUDGET) { + return 0; + } + + return N64_THA_BUDGET - total; +} \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.h b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.h new file mode 100644 index 00000000000..0f3ab4812a8 --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_arena_sizing.h @@ -0,0 +1,31 @@ +#pragma once + +#include "z64.h" + +#ifdef __cplusplus +extern "C" { + + + +#endif + +// -------------------------------------------------------------------------------------------------------------------- +// Arena size computation +// +// Computes the N64 ZeldaArena size from first principles: +// arena = THA_BUDGET - sum(all THA consumers at N64 sizes) +// +// All per-version constants are derived from OTR blobs at runtime: +// kaleidoOverlayVramSize -> N64SizeData_GetKaleidoVramSize() (misc/n64_memory/kaleido_vram_size) +// parameterStaticSize -> N64SizeData_GetDmaFileSize(...) (misc/n64_memory/dma_sizes) +// effectSsSize -> N64_SIZEOF_EFFECT_SS (constant 0x60, all N64 versions) +// +// Called from N64Mem_Reset after Play_Init has populated scene data. Returns 0 if THA consumption exceeds budget +// (should not happen on valid scenes). +// -------------------------------------------------------------------------------------------------------------------- + +uint32_t ArenaSizing_ComputeN64ArenaSize(PlayState* play); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.c b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.c new file mode 100644 index 00000000000..c715d5c31d1 --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.c @@ -0,0 +1,319 @@ +#include "n64_shadow_arena.h" + +#include +#include + +// -------------------------------------------------------------------------------------------------------------------- +// Internal node layout (not exposed -- heap viewer uses offset + size queries, not direct struct access). +// +// Fixed header fields (0x10 bytes, identical on retail and debug): +// 0x00 int16_t magic +// 0x02 int16_t isFree +// 0x04 uint32_t size (Payload bytes, excluding node header) +// 0x08 uint32_t next (Offset into buffer, SHADOW_NULL = end) +// 0x0C uint32_t prev (Offset into buffer, SHADOW_NULL = head) +// +// On debug builds, an additional 0x20 bytes of debug fields follow (filename, line, threadId, arena, time). +// The total node spacing is arena->nodeSize: 0x10 for retail, 0x30 for debug. +// -------------------------------------------------------------------------------------------------------------------- + +typedef struct ShadowNode { + int16_t magic; + int16_t isFree; + uint32_t size; + uint32_t next; + uint32_t prev; +} ShadowNode; // 0x10 + +static_assert(sizeof(ShadowNode) == 0x10, "ShadowNode header must be exactly 0x10 bytes"); + +#define NODE_MAGIC 0x7373 +#define ALIGN16(x) (((x) + 0xF) & ~0xF) + +// -------------------------------------------------------------------------------------------------------------------- +// Offset helpers +// -------------------------------------------------------------------------------------------------------------------- + +static ShadowNode* NodeAt(ShadowArena* arena, uint32_t offset) { + return (ShadowNode*)(arena->buffer + offset); +} + +static int32_t IsNodeValid(ShadowArena* arena, uint32_t offset) { + if (offset == SHADOW_NULL) { + return 0; + } + + if (offset + arena->nodeSize > arena->bufferSize) { + return 0; + } + + return NodeAt(arena, offset)->magic == NODE_MAGIC; +} + +static uint32_t NodeGetNext(ShadowArena* arena, uint32_t offset) { + const ShadowNode* node = NodeAt(arena, offset); + if (node->next != SHADOW_NULL && IsNodeValid(arena, node->next)) { + return node->next; + } + + return SHADOW_NULL; +} + +static uint32_t NodeGetPrev(ShadowArena* arena, uint32_t offset) { + const ShadowNode* node = NodeAt(arena, offset); + if (node->prev != SHADOW_NULL && IsNodeValid(arena, node->prev)) { + return node->prev; + } + + return SHADOW_NULL; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Init / Destroy +// -------------------------------------------------------------------------------------------------------------------- + +void ShadowArena_Init(ShadowArena* arena, uint32_t size, uint32_t nodeSize) { + // Match N64's alignment: Round start up to 16, round size down to 16. Since we control the buffer, start is + // effectively offset 0 after alignment. We just ensure the usable size is 16-byte aligned. + const uint32_t alignedSize = size & ~0xF; + + arena->buffer = (uint8_t*)malloc(alignedSize); + if (!arena->buffer) { + memset(arena, 0, sizeof(ShadowArena)); + return; + } + + memset(arena->buffer, 0, alignedSize); + arena->bufferSize = alignedSize; + arena->nodeSize = nodeSize; + + // Single free node spanning the entire buffer minus one header. + ShadowNode* first = NodeAt(arena, 0); + first->magic = NODE_MAGIC; + first->isFree = 1; + first->size = alignedSize - nodeSize; + first->next = SHADOW_NULL; + first->prev = SHADOW_NULL; + arena->head = 0; +} + +void ShadowArena_Destroy(ShadowArena* arena) { + if (arena->buffer) { + free(arena->buffer); + } + + memset(arena, 0, sizeof(ShadowArena)); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Malloc: First-fit forward (matches N64 __osMalloc) +// -------------------------------------------------------------------------------------------------------------------- + +uint32_t ShadowArena_Malloc(ShadowArena* arena, uint32_t size) { + uint32_t iterOff = 0; + ShadowNode* iter = NULL; + uint32_t blockSize = 0; + + size = ALIGN16(size); + blockSize = size + arena->nodeSize; + + iterOff = arena->head; + while (iterOff != SHADOW_NULL) { + iter = NodeAt(arena, iterOff); + if (iter->isFree && iter->size >= size) { + // Split if remainder can hold a new node + payload. + if (blockSize < iter->size) { + const uint32_t newOff = iterOff + blockSize; + ShadowNode* newNode = NodeAt(arena, newOff); + + newNode->magic = NODE_MAGIC; + newNode->isFree = 1; + newNode->size = iter->size - blockSize; + newNode->next = iter->next; + newNode->prev = iterOff; + + if (newNode->next != SHADOW_NULL) { + NodeAt(arena, newNode->next)->prev = newOff; + } + + iter->next = newOff; + iter->size = size; + } + + iter->isFree = 0; + + // Return offset to data area (past the node header). + return iterOff + arena->nodeSize; + } + + iterOff = NodeGetNext(arena, iterOff); + } + + return SHADOW_NULL; +} + +// -------------------------------------------------------------------------------------------------------------------- +// MallocR: First-fit backward (matches N64 __osMallocR) +// -------------------------------------------------------------------------------------------------------------------- + +uint32_t ShadowArena_MallocR(ShadowArena* arena, uint32_t size) { + uint32_t iterOff = 0; + uint32_t nextOff = 0; + ShadowNode* iter = NULL; + uint32_t blockSize = 0; + + size = ALIGN16(size); + + // Walk to tail. + iterOff = arena->head; + nextOff = NodeGetNext(arena, iterOff); + while (nextOff != SHADOW_NULL) { + iterOff = nextOff; + nextOff = NodeGetNext(arena, nextOff); + } + + // Walk backward looking for a fit. + while (iterOff != SHADOW_NULL) { + iter = NodeAt(arena, iterOff); + if (iter->isFree && iter->size >= size) { + blockSize = size + arena->nodeSize; + + // Split: Carve the allocation from the TOP of this free block. + if (blockSize < iter->size) { + const uint32_t newOff = iterOff + (iter->size - size); + ShadowNode* newNode = NodeAt(arena, newOff); + + newNode->magic = NODE_MAGIC; + newNode->isFree = 0; + newNode->size = size; + newNode->next = iter->next; + newNode->prev = iterOff; + + if (newNode->next != SHADOW_NULL) { + NodeAt(arena, newNode->next)->prev = newOff; + } + + iter->next = newOff; + iter->size -= blockSize; + + return newOff + arena->nodeSize; + } + + // No split, use the whole block. + iter->isFree = 0; + return iterOff + arena->nodeSize; + } + + iterOff = NodeGetPrev(arena, iterOff); + } + + return SHADOW_NULL; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Free: Adjacent block coalescing (matches N64 _osFree) +// -------------------------------------------------------------------------------------------------------------------- + +void ShadowArena_Free(ShadowArena* arena, uint32_t dataOffset) { + uint32_t nodeOff = 0; + ShadowNode* node = NULL; + uint32_t nextOff = 0; + uint32_t prevOff = 0; + + if (dataOffset == SHADOW_NULL) { + return; + } + + nodeOff = dataOffset - arena->nodeSize; + node = NodeAt(arena, nodeOff); + + if (node->magic != NODE_MAGIC) { + return; + } + + if (node->isFree) { + return; + } + + node->isFree = 1; + + // Forward coalesce: Merge with next if it's free. + nextOff = node->next; + if (nextOff != SHADOW_NULL) { + const ShadowNode* next = NodeAt(arena, nextOff); + if (next->isFree) { + if (next->next != SHADOW_NULL) { + NodeAt(arena, next->next)->prev = nodeOff; + } + + node->size += next->size + arena->nodeSize; + node->next = next->next; + } + } + + // Backward coalesce: Merge into prev if it's free. + prevOff = node->prev; + if (prevOff != SHADOW_NULL) { + ShadowNode* prev = NodeAt(arena, prevOff); + if (prev->isFree) { + prev->size += node->size + arena->nodeSize; + prev->next = node->next; + + if (node->next != SHADOW_NULL) { + NodeAt(arena, node->next)->prev = prevOff; + } + } + } +} + +// -------------------------------------------------------------------------------------------------------------------- +// Query +// -------------------------------------------------------------------------------------------------------------------- + +void ShadowArena_GetSizes(ShadowArena* arena, uint32_t* outMaxFree, uint32_t* outFree, uint32_t* outAlloc) { + uint32_t iterOff = 0; + + *outMaxFree = 0; + *outFree = 0; + *outAlloc = 0; + + iterOff = arena->head; + while (iterOff != SHADOW_NULL) { + const ShadowNode* node = NodeAt(arena, iterOff); + if (node->isFree) { + *outFree += node->size; + if (node->size > *outMaxFree) { + *outMaxFree = node->size; + } + } else { + *outAlloc += node->size; + } + + iterOff = NodeGetNext(arena, iterOff); + } +} + +uint32_t ShadowArena_GetHead(ShadowArena* arena) { + return arena->head; +} + +int32_t ShadowArena_GetNodeInfo(ShadowArena* arena, uint32_t offset, int32_t* outIsFree, uint32_t* outSize, + uint32_t* outNext) { + if (offset == SHADOW_NULL || offset + arena->nodeSize > arena->bufferSize) { + return 0; + } + + const ShadowNode* node = NodeAt(arena, offset); + if (node->magic != NODE_MAGIC) { + return 0; + } + + *outIsFree = node->isFree; + *outSize = node->size; + *outNext = node->next != SHADOW_NULL && IsNodeValid(arena, node->next) ? node->next : SHADOW_NULL; + return 1; +} + +uint32_t ShadowArena_GetBufferSize(ShadowArena* arena) { + return arena->bufferSize; +} \ No newline at end of file diff --git a/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.h b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.h new file mode 100644 index 00000000000..c6327896ef4 --- /dev/null +++ b/soh/soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { + + + +#endif + +// Sentinel value for null offsets (no valid node can live at 0xFFFFFFFF in a buffer that's only ~245KB). +#define SHADOW_NULL 0xFFFFFFFF + +// Default node size for the shadow arena. Overridden at init from OTR data. +// Retail N64 (no debug fields): 0x10 +// GC Debug (debug fields): 0x30 +#define SHADOW_NODE_SIZE_RETAIL 0x10 +#define SHADOW_NODE_SIZE_DEBUG 0x30 + +typedef struct ShadowArena { + uint8_t* buffer; + uint32_t head; // Offset to first node + uint32_t bufferSize; + uint32_t nodeSize; // Per-version ArenaNode size (set at init from OTR data) +} ShadowArena; + +// Allocate backing buffer and initialize with a single free node. nodeSize is the N64 ArenaNode size for this ROM +// version (0x10 for retail, 0x30 for debug). +void ShadowArena_Init(ShadowArena* arena, uint32_t size, uint32_t nodeSize); + +// Free the backing buffer and zero the struct. +void ShadowArena_Destroy(ShadowArena* arena); + +// First-fit forward allocation. Returns offset to data area, or SHADOW_NULL on failure. Matches N64 __osMalloc. +uint32_t ShadowArena_Malloc(ShadowArena* arena, uint32_t size); + +// First-fit backward allocation. Returns offset to data area, or SHADOW_NULL on failure. Matches N64 __osMallocR. +uint32_t ShadowArena_MallocR(ShadowArena* arena, uint32_t size); + +// Free a shadow allocation by data offset. Coalesces adjacent free blocks. Matches N64 __osFree. +void ShadowArena_Free(ShadowArena* arena, uint32_t dataOffset); + +// Query arena statistics. +void ShadowArena_GetSizes(ShadowArena* arena, uint32_t* outMaxFree, uint32_t* outFree, uint32_t* outAlloc); + +// Get the head node offset for external traversal (e.g., heap viewer). +uint32_t ShadowArena_GetHead(ShadowArena* arena); + +// Query a node's info by offset. Returns true on success, false if invalid. Used by the heap viewer to walk the +// shadow without exposing internals. +int32_t ShadowArena_GetNodeInfo(ShadowArena* arena, uint32_t offset, int32_t* outIsFree, uint32_t* outSize, + uint32_t* outNext); + +// Get the total buffer size. +uint32_t ShadowArena_GetBufferSize(ShadowArena* arena); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/soh/soh/Enhancements/debugger/HeapViewerWindow.cpp b/soh/soh/Enhancements/debugger/HeapViewerWindow.cpp new file mode 100644 index 00000000000..3737dc1a1a9 --- /dev/null +++ b/soh/soh/Enhancements/debugger/HeapViewerWindow.cpp @@ -0,0 +1,562 @@ +#include "HeapViewerWindow.hpp" + +#include +#include + +#include +#include + +#include "soh/OTRGlobals.h" +#include "soh/ActorDB.h" + +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" + +extern "C" { +#include "z64.h" +#include "functions.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/n64_shadow_arena.h" +} + +// ----------------------------------------------------------------------------------------------------------------- +// Block data +// ----------------------------------------------------------------------------------------------------------------- + +struct BlockInfo { + uint32_t offset; + size_t size; + bool isFree; + uint8_t type; // N64MEM_BLOCK_* (shadow only) + int16_t actorId; // Original actor ID (shadow only), -1 if unknown + const char* actorName; + bool isPinned; + bool isGhost; // Pinned block that was freed -- shown as a grayed-out row + uint32_t heapIndex; // Original index in heap order (for block map cross-reference) +}; + +static std::vector sBlocks; +static std::vector sDisplayOrder; // Indices into sBlocks: pinned first, then unpinned +static uint32_t sPinnedCount = 0; +static std::set> sPinnedBlocks; // {actorId, blockType} pairs +static size_t sAllocTotal = 0; +static size_t sFreeTotal = 0; +static size_t sLargestFree = 0; +static uint32_t sNodeCount = 0; +static size_t sArenaSize = 0; +static size_t sPreviousAlloc = 0; +static uint32_t sCycleCount = 0; +static int32_t sLastDelta = 0; +static bool sIsShadow = false; + +static const char* GetActorName(int16_t actorId) { + if (actorId < 0) { + return nullptr; + } + + const auto& entry = ActorDB::Instance->RetrieveEntry(actorId); + return entry.desc.empty() ? entry.entry.name : entry.desc.c_str(); +} + +static const char* BlockTypeName(uint8_t type) { + switch (type) { + case N64MEM_BLOCK_INSTANCE: + return "inst"; + + case N64MEM_BLOCK_OVERLAY: + return "ovl"; + + case N64MEM_BLOCK_SUBSIDIARY: + return "sub"; + + case N64MEM_BLOCK_EFFECT: + return "efx"; + + case N64MEM_BLOCK_ABSOLUTE: + return "abs"; + + default: + return "???"; + } +} + +static const char* sEffectNames[] = { + "Dust", "KiraKira", "Bomb", "Bomb2", "Blast", "G_Spk", "D_Fire", "Bubble", + "(unset)", "G_Ripple", "G_Splash", "G_Magma", "G_Fire", "Lightning", "Dt_Bubble", + "Hahen", "Stick", "Sibuki", "Sibuki2", "G_Magma2", "Stone1", "HitMark", + "Fhg_Flash", "K_Fire", "Solder_Srch_Ball", "Kakera", "Ice_Piece", "En_Ice", + "Fire_Tail", "En_Fire", "Extra", "Fcircle", "Dead_Db", "Dead_Dd", "Dead_Ds", + "Dead_Sound", "Ice_Smoke", +}; + +static const char* GetBlockDisplayName(const BlockInfo& block) { + if (block.isFree && !block.isGhost) { + return nullptr; + } + + if (block.type == N64MEM_BLOCK_EFFECT && block.actorId >= 0 && + block.actorId < static_cast(std::size(sEffectNames))) { + return sEffectNames[block.actorId]; + } + + return block.actorName; +} + +static void CollectZeldaArenaBlocks() { + sBlocks.clear(); + sAllocTotal = 0; + sFreeTotal = 0; + sLargestFree = 0; + sNodeCount = 0; + sArenaSize = 0; + sIsShadow = false; + + ArenaNode* node = ZeldaArena_GetHead(); + if (!node) { + return; + } + + while (node) { + BlockInfo block; + block.offset = static_cast(reinterpret_cast(node) + sizeof(ArenaNode)); + block.size = node->size; + block.isFree = node->isFree; + block.type = N64MEM_BLOCK_FREE; + block.actorId = -1; + block.actorName = nullptr; + + sBlocks.push_back(block); + sArenaSize += sizeof(ArenaNode) + node->size; + sNodeCount++; + + if (node->isFree) { + sFreeTotal += node->size; + if (node->size > sLargestFree) { + sLargestFree = node->size; + } + } else { + sAllocTotal += node->size; + } + + node = node->next; + } + + if (sPreviousAlloc != 0 && sAllocTotal != sPreviousAlloc) { + sLastDelta = static_cast(sAllocTotal) - static_cast(sPreviousAlloc); + ++sCycleCount; + } + + sPreviousAlloc = sAllocTotal; +} + +static void CollectShadowBlocks() { + sBlocks.clear(); + sAllocTotal = 0; + sFreeTotal = 0; + sLargestFree = 0; + sNodeCount = 0; + sIsShadow = true; + + ShadowArena* shadow = N64Mem_GetShadowArena(); + if (!shadow) { + sArenaSize = 0; + return; + } + + sArenaSize = ShadowArena_GetBufferSize(shadow); + uint32_t offset = ShadowArena_GetHead(shadow); + + while (offset != SHADOW_NULL) { + int32_t isFree = 0; + uint32_t size = 0; + uint32_t next = SHADOW_NULL; + + if (!ShadowArena_GetNodeInfo(shadow, offset, &isFree, &size, &next)) { + break; + } + + BlockInfo block; + block.offset = offset + shadow->nodeSize; + block.size = size; + block.isFree = isFree != 0; + block.type = N64MEM_BLOCK_FREE; + block.actorId = -1; + block.actorName = nullptr; + + if (!isFree) { + uint8_t type = 0; + int16_t actorId = -1; + if (N64Mem_GetBlockInfo(block.offset, &type, &actorId)) { + block.type = type; + block.actorId = actorId; + block.actorName = GetActorName(actorId); + } + } + + sBlocks.push_back(block); + sNodeCount++; + + if (isFree) { + sFreeTotal += size; + if (size > sLargestFree) { + sLargestFree = size; + } + } else { + sAllocTotal += size; + } + + offset = next; + } + + if (sPreviousAlloc != 0 && sAllocTotal != sPreviousAlloc) { + sLastDelta = static_cast(sAllocTotal) - static_cast(sPreviousAlloc); + ++sCycleCount; + } + + sPreviousAlloc = sAllocTotal; +} + +// Build display order: assign heap indices, mark pinned blocks, add ghosts for freed pins, sort pinned to top. +static void BuildDisplayOrder() { + sDisplayOrder.clear(); + sPinnedCount = 0; + + // Track which pin keys have a live block. + std::set> matchedPins; + + for (size_t i = 0; i < sBlocks.size(); ++i) { + sBlocks[i].heapIndex = static_cast(i); + sBlocks[i].isGhost = false; + std::pair key = { sBlocks[i].actorId, sBlocks[i].type }; + sBlocks[i].isPinned = !sBlocks[i].isFree && sPinnedBlocks.contains(key); + if (sBlocks[i].isPinned) { + matchedPins.insert(key); + } + } + + // Add ghost entries for pinned blocks that no longer exist. + for (const auto& pin : sPinnedBlocks) { + if (!matchedPins.contains(pin)) { + BlockInfo ghost; + ghost.offset = 0; + ghost.size = 0; + ghost.isFree = true; + ghost.type = pin.second; + ghost.actorId = pin.first; + ghost.actorName = GetActorName(pin.first); + ghost.isPinned = true; + ghost.isGhost = true; + ghost.heapIndex = 0; + sBlocks.push_back(ghost); + } + } + + // Pinned first (heap order preserved within group, ghosts at end of pinned section). + for (size_t i = 0; i < sBlocks.size(); ++i) { + if (sBlocks[i].isPinned && !sBlocks[i].isGhost) { + sDisplayOrder.push_back(i); + ++sPinnedCount; + } + } + + for (size_t i = 0; i < sBlocks.size(); ++i) { + if (sBlocks[i].isGhost) { + sDisplayOrder.push_back(i); + ++sPinnedCount; + } + } + + // Then unpinned. + for (size_t i = 0; i < sBlocks.size(); ++i) { + if (!sBlocks[i].isPinned) { + sDisplayOrder.push_back(i); + } + } +} + +// ----------------------------------------------------------------------------------------------------------------- +// Colors +// ----------------------------------------------------------------------------------------------------------------- + +static int32_t BlockColor(const BlockInfo& block) { + if (block.isFree) { + return IM_COL32(60, 180, 80, 255); + } + + if (!sIsShadow) { + return IM_COL32(200, 60, 60, 255); + } + + switch (block.type) { + case N64MEM_BLOCK_INSTANCE: + return IM_COL32(200, 60, 60, 255); + + case N64MEM_BLOCK_OVERLAY: + return IM_COL32(220, 140, 40, 255); + + case N64MEM_BLOCK_EFFECT: + return IM_COL32(60, 120, 200, 255); + + case N64MEM_BLOCK_SUBSIDIARY: + return IM_COL32(200, 200, 60, 255); + + case N64MEM_BLOCK_ABSOLUTE: + return IM_COL32(160, 60, 200, 255); + + default: + return IM_COL32(120, 120, 120, 255); + } +} + +static ImU32 ColorNode() { + return IM_COL32(80, 80, 80, 255); +} + +static ImU32 ColorBorder() { + return IM_COL32(40, 40, 40, 255); +} + +// ----------------------------------------------------------------------------------------------------------------- +// Drawing +// ----------------------------------------------------------------------------------------------------------------- + +void HeapViewerWindow::DrawElement() { + // Single arena view: Shadow when active, ZeldaArena when not. + const bool isShadowArena = N64Mem_IsActive(); + const uint32_t nodeHeaderSize = isShadowArena ? N64Mem_GetShadowArena()->nodeSize : sizeof(ArenaNode); + + if (isShadowArena) { + CollectShadowBlocks(); + } else { + CollectZeldaArenaBlocks(); + } + + BuildDisplayOrder(); + + if (isShadowArena) { + if (ImGui::Button("Reset Tracking")) { + sPreviousAlloc = 0; + sCycleCount = 0; + sLastDelta = 0; + } + + if (!sPinnedBlocks.empty()) { + ImGui::SameLine(); + if (ImGui::Button("Clear Pins")) { + sPinnedBlocks.clear(); + } + } + } + + if (sBlocks.empty()) { + ImGui::Text("Arena is not initialized."); + return; + } + + // --- Stats --- + const f32 fragPercent = sFreeTotal > 0 + ? (1.0f - static_cast(sLargestFree) / static_cast(sFreeTotal)) * 100.0f + : 0.0f; + + ImGui::Text("Arena: 0x%llX (%llu KB) | Nodes: %u", sArenaSize, sArenaSize / 1024, sNodeCount); + ImGui::Text("Alloc: 0x%llX (%llu KB) | Free: 0x%llX (%llu KB) | Largest: 0x%llX (%llu KB)", + sAllocTotal, sAllocTotal / 1024, + sFreeTotal, sFreeTotal / 1024, + sLargestFree, sLargestFree / 1024); + + ImGui::Text("Fragmentation: %.1f%%", fragPercent); + ImGui::Text("Cycle: %u | Last delta: %s0x%X (%d bytes)", + sCycleCount, + sLastDelta >= 0 ? "+" : "-", + static_cast(std::abs(sLastDelta)), + sLastDelta); + + // --- Utilization bar --- + const f32 utilization = sArenaSize > 0 ? static_cast(sAllocTotal) / static_cast(sArenaSize) : 0.0f; + ImGui::ProgressBar(utilization, ImVec2(-1, 0), + fmt::format("{:.1f}% utilized", utilization * 100.0f).c_str()); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Block map --- + ImGui::Text("Block Map"); + + constexpr f32 mapHeight = 40.0f; + const f32 availWidth = ImGui::GetContentRegionAvail().x; + const ImVec2 mapPos = ImGui::GetCursorScreenPos(); + + ImGui::Dummy(ImVec2(availWidth, mapHeight)); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(mapPos, ImVec2(mapPos.x + availWidth, mapPos.y + mapHeight), ColorBorder()); + + f32 xCursor = 0.0f; + int32_t hoveredBlock = -1; + + for (size_t i = 0; i < sBlocks.size(); ++i) { + f32 nodeWidth = static_cast(nodeHeaderSize) / static_cast(sArenaSize) * availWidth; + f32 blockWidth = static_cast(sBlocks[i].size) / static_cast(sArenaSize) * availWidth; + + if (nodeWidth < 1.0f) { + nodeWidth = 1.0f; + } + + if (blockWidth < 1.0f && sBlocks[i].size > 0) { + blockWidth = 1.0f; + } + + f32 x0 = mapPos.x + xCursor; + f32 x1 = x0 + nodeWidth; + drawList->AddRectFilled(ImVec2(x0, mapPos.y), ImVec2(x1, mapPos.y + mapHeight), ColorNode()); + xCursor += nodeWidth; + + x0 = mapPos.x + xCursor; + x1 = x0 + blockWidth; + drawList->AddRectFilled(ImVec2(x0, mapPos.y), ImVec2(x1, mapPos.y + mapHeight), BlockColor(sBlocks[i])); + + // White outline for pinned blocks. + if (sBlocks[i].isPinned) { + drawList->AddRect(ImVec2(x0, mapPos.y), ImVec2(x1, mapPos.y + mapHeight), + IM_COL32(255, 255, 255, 255), 0.0f, 0, 2.0f); + } + + const f32 fullX0 = mapPos.x + xCursor - nodeWidth; + ImVec2 blockMin(fullX0, mapPos.y); + if (ImVec2 blockMax(x1, mapPos.y + mapHeight); ImGui::IsMouseHoveringRect(blockMin, blockMax)) { + hoveredBlock = static_cast(i); + } + + xCursor += blockWidth; + } + + if (hoveredBlock >= 0) { + const auto& block = sBlocks[hoveredBlock]; + ImGui::BeginTooltip(); + + if (block.isFree) { + ImGui::Text("Block %d: FREE", hoveredBlock); + } else if (sIsShadow && GetBlockDisplayName(block)) { + ImGui::Text("Block %d: %s (%s)", hoveredBlock, GetBlockDisplayName(block), BlockTypeName(block.type)); + } else if (sIsShadow) { + ImGui::Text("Block %d: %s", hoveredBlock, BlockTypeName(block.type)); + } else { + ImGui::Text("Block %d: ALLOCATED", hoveredBlock); + } + + ImGui::Text("Offset: 0x%X", block.offset); + ImGui::Text("Size: 0x%llX (%llu bytes)", block.size, block.size); + ImGui::EndTooltip(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Block list --- + ImGui::Text("Block List (%u blocks%s)", sNodeCount, + sPinnedCount > 0 ? fmt::format(", {} pinned", sPinnedCount).c_str() : ""); + + if (const int32_t columnCount = sIsShadow ? 5 : 4; ImGui::BeginTable("##blocks", columnCount, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg + | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_Resizable, + ImVec2(0, ImGui::GetContentRegionAvail().y))) { + ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed, 40.0f); + + if (sIsShadow) { + ImGui::TableSetupColumn("Actor", ImGuiTableColumnFlags_WidthStretch); + } + + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 140.0f); + ImGui::TableSetupColumn("Offset", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupScrollFreeze(0, 1 + static_cast(sPinnedCount)); + ImGui::TableHeadersRow(); + + for (size_t di = 0; di < sDisplayOrder.size(); ++di) { + const size_t blockIdx = sDisplayOrder[di]; + const auto& block = sBlocks[blockIdx]; + ImGui::TableNextRow(); + + // Tint pinned rows. + if (block.isPinned) { + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, + block.isGhost ? IM_COL32(255, 60, 60, 15) : IM_COL32(255, 255, 255, 20)); + } + + // # -- Click to pin/unpin (allocated shadow blocks and ghosts) + ImGui::TableNextColumn(); + + if (sIsShadow && ((!block.isFree && block.actorId >= 0) || block.isGhost)) { + ImGui::PushID(static_cast(di)); + if (ImGui::Selectable(fmt::format("{}{}", block.isPinned ? "\xF0\x9F\x93\x8C " : "", + block.isGhost ? "--" : std::to_string(block.heapIndex)).c_str(), + block.isPinned, + ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) { + std::pair key = { block.actorId, block.type }; + + if (block.isPinned) { + sPinnedBlocks.erase(key); + } else { + sPinnedBlocks.insert(key); + } + } + ImGui::PopID(); + } else { + ImGui::Text("%u", block.heapIndex); + } + + // Actor (shadow only) + if (sIsShadow) { + ImGui::TableNextColumn(); + if (block.isGhost) { + const char* displayName = GetBlockDisplayName(block); + ImGui::TextDisabled("%s", displayName ? displayName : "???"); + } else if (block.isFree) { + ImGui::TextDisabled("--"); + } else if (const char* displayName = GetBlockDisplayName(block)) { + ImGui::Text("%s", displayName); + if (ImGui::IsItemHovered() && block.actorId >= 0 && block.type != N64MEM_BLOCK_EFFECT) { + const auto& entry = ActorDB::Instance->RetrieveEntry(block.actorId); + ImGui::SetTooltip("%s (0x%04X)", entry.entry.name, block.actorId); + } + } else { + ImGui::TextDisabled("--"); + } + } + + // Type + ImGui::TableNextColumn(); + if (block.isGhost) { + ImGui::TextColored(ImVec4(0.6f, 0.2f, 0.2f, 1.0f), "freed"); + } else if (block.isFree) { + ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.3f, 1.0f), "free"); + } else if (sIsShadow) { + const int32_t color = BlockColor(block); + ImVec4 colorVec = ImGui::ColorConvertU32ToFloat4(color); + ImGui::TextColored(colorVec, "%s", BlockTypeName(block.type)); + } else { + ImGui::TextColored(ImVec4(0.8f, 0.25f, 0.25f, 1.0f), "alloc"); + } + + // Size + ImGui::TableNextColumn(); + if (block.isGhost) { + ImGui::TextDisabled("--"); + } else { + ImGui::Text("0x%llX (%llu)", block.size, block.size); + } + + // Offset + ImGui::TableNextColumn(); + if (block.isGhost) { + ImGui::TextDisabled("--"); + } else { + ImGui::Text("0x%X", block.offset); + } + } + + ImGui::EndTable(); + } +} \ No newline at end of file diff --git a/soh/soh/Enhancements/debugger/HeapViewerWindow.hpp b/soh/soh/Enhancements/debugger/HeapViewerWindow.hpp new file mode 100644 index 00000000000..fa8be5b6c46 --- /dev/null +++ b/soh/soh/Enhancements/debugger/HeapViewerWindow.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +class HeapViewerWindow : public Ship::GuiWindow { +public: + using GuiWindow::GuiWindow; + + void InitElement() override { + } + + void DrawElement() override; + + void UpdateElement() override { + } +}; \ No newline at end of file diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 89224c0ab57..876f4946b87 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -504,29 +504,33 @@ void SaveManager::StartupCheckAndInitMeta(int fileNum) { output.close(); saveMtx.unlock(); } - s16 major = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; - s16 minor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; - s16 patch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; - // block loading outdated rando save - if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor && patch == gBuildVersionPatch)) { - std::string newFileName = - Ship::Context::GetPathRelativeToAppDirectory("Save") + - ("/file" + std::to_string(fileNum + 1) + "-" + std::to_string(GetUnixTimestamp()) + ".bak"); + + if (metaSaveBlock["sections"].contains("sohStats") && metaSaveBlock["sections"]["sohStats"].contains("data")) { + s16 major = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; + s16 minor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; + s16 patch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; + // block loading outdated rando save + if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor && patch == gBuildVersionPatch)) { + std::string newFileName = + Ship::Context::GetPathRelativeToAppDirectory("Save") + + ("/file" + std::to_string(fileNum + 1) + "-" + std::to_string(GetUnixTimestamp()) + ".bak"); #if defined(__SWITCH__) || defined(__WIIU__) - copy_file(fileName.c_str(), newFileName.c_str()); - std::filesystem::remove(fileName); + copy_file(fileName.c_str(), newFileName.c_str()); + std::filesystem::remove(fileName); #else - std::filesystem::rename(fileName, newFileName); + std::filesystem::rename(fileName, newFileName); #endif - SohGui::RegisterPopup("Outdated Randomizer Save", - "The SoH version in the file in slot " + std::to_string(fileNum + 1) + - " does not match the currently running version.\n" + - "Non-matching rando saves are unsupported, and the file has been renamed to\n" + - " " + newFileName + "\n" + - "If this was not in error, the file should be deleted."); - return; + SohGui::RegisterPopup("Outdated Randomizer Save", + "The SoH version in the file in slot " + std::to_string(fileNum + 1) + + " does not match the currently running version.\n" + + "Non-matching rando saves are unsupported, and the file has been renamed to\n" + + " " + newFileName + "\n" + + "If this was not in error, the file should be deleted."); + return; + } } } + bool isRando = metaSaveBlock["fileType"] == FILE_TYPE_SAVE_RANDO; fileMetaInfo[fileNum].valid = true; @@ -573,12 +577,19 @@ void SaveManager::StartupCheckAndInitMeta(int fileNum) { fileMetaInfo[fileNum].requiresOriginal = randoBlock["masterQuestDungeonCount"] < 12; } - fileMetaInfo[fileNum].buildVersionMajor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; - fileMetaInfo[fileNum].buildVersionMinor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; - fileMetaInfo[fileNum].buildVersionPatch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; - SohUtils::CopyStringToCharArray(fileMetaInfo[fileNum].buildVersion, - metaSaveBlock["sections"]["sohStats"]["data"]["buildVersion"], - ARRAY_COUNT(fileMetaInfo[fileNum].buildVersion)); + if (metaSaveBlock["sections"].contains("sohStats") && metaSaveBlock["sections"]["sohStats"].contains("data")) { + fileMetaInfo[fileNum].buildVersionMajor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; + fileMetaInfo[fileNum].buildVersionMinor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; + fileMetaInfo[fileNum].buildVersionPatch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; + SohUtils::CopyStringToCharArray(fileMetaInfo[fileNum].buildVersion, + metaSaveBlock["sections"]["sohStats"]["data"]["buildVersion"], + ARRAY_COUNT(fileMetaInfo[fileNum].buildVersion)); + } else { + fileMetaInfo[fileNum].buildVersionMajor = 0; + fileMetaInfo[fileNum].buildVersionMinor = 0; + fileMetaInfo[fileNum].buildVersionPatch = 0; + fileMetaInfo[fileNum].buildVersion[0] = '\0'; + } } void SaveManager::InitMeta(int fileNum) { @@ -1154,7 +1165,15 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se sectionHandlerPair.second.func(saveContext, sectionID, true); } } else { + if (auto it = sectionSaveHandlers.find(sectionID); it == sectionSaveHandlers.end()) { + SPDLOG_ERROR("[SaveManager] SaveFileThreaded: sectionID {} not found in sectionSaveHandlers", sectionID); + delete saveContext; + saveMtx.unlock(); + return; + } + SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second; + auto& sectionName = svi.name; auto sectionVersion = svi.version; // If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp index 04c47b4b7e6..c3bd27d4545 100644 --- a/soh/soh/SohGui/SohGui.cpp +++ b/soh/soh/SohGui/SohGui.cpp @@ -26,6 +26,7 @@ #include "soh/Enhancements/TimeDisplay/TimeDisplay.h" #include "soh/Enhancements/mod_menu.h" #include "soh/Network/Anchor/Anchor.h" +#include "soh/Enhancements/debugger/HeapViewerWindow.hpp" namespace SohGui { @@ -78,6 +79,7 @@ std::shared_ptr mHookDebuggerWindow; std::shared_ptr mDLViewerWindow; std::shared_ptr mValueViewerWindow; std::shared_ptr mMessageViewerWindow; +std::shared_ptr mHeapViewerWindow; std::shared_ptr mGameplayStatsWindow; std::shared_ptr mCheckTrackerSettingsWindow; std::shared_ptr mCheckTrackerWindow; @@ -166,7 +168,9 @@ void SetupGuiElements() { gui->AddGuiWindow(mMessageViewerWindow); mGameplayStatsWindow = std::make_shared(CVAR_WINDOW("GameplayStats"), "Gameplay Stats", ImVec2(480, 550)); - gui->AddGuiWindow(mGameplayStatsWindow); + mHeapViewerWindow = + std::make_shared(CVAR_WINDOW("HeapViewer"), "Heap Viewer", ImVec2(520, 600)); + gui->AddGuiWindow(mHeapViewerWindow); mCheckTrackerWindow = std::make_shared(CVAR_WINDOW("CheckTracker"), "Check Tracker", ImVec2(400, 540)); gui->AddGuiWindow(mCheckTrackerWindow); @@ -211,6 +215,7 @@ void Destroy() { mEntranceTrackerSettingsWindow = nullptr; mCheckTrackerWindow = nullptr; mCheckTrackerSettingsWindow = nullptr; + mHeapViewerWindow = nullptr; mGameplayStatsWindow = nullptr; mDLViewerWindow = nullptr; mValueViewerWindow = nullptr; diff --git a/soh/soh/SohGui/SohMenuDevTools.cpp b/soh/soh/SohGui/SohMenuDevTools.cpp index c5b88ce8fbc..4bec3157ac1 100644 --- a/soh/soh/SohGui/SohMenuDevTools.cpp +++ b/soh/soh/SohGui/SohMenuDevTools.cpp @@ -175,6 +175,15 @@ void SohMenu::AddMenuDevTools() { .HideInSearch(true) .Options(WindowButtonOptions().Tooltip("Enables the separate Hook Debugger Window.")); + // Gfx Debugger + path.sidebarName = "Gfx Debugger"; + AddSidebarEntry("Dev Tools", path.sidebarName, 1); + AddWidget(path, "Popout Gfx Debugger", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("SohGfxDebugger")) + .WindowName("GfxDebugger##SoH") + .HideInSearch(true) + .Options(WindowButtonOptions().Tooltip("Enables the separate Gfx Debugger Window.")); + // Collision Viewer path.sidebarName = "Collision Viewer"; AddSidebarEntry("Dev Tools", path.sidebarName, 2); @@ -220,14 +229,14 @@ void SohMenu::AddMenuDevTools() { .HideInSearch(true) .Options(WindowButtonOptions().Tooltip("Enables the separate Message Viewer Window.")); - // Gfx Debugger - path.sidebarName = "Gfx Debugger"; + // Heap Viewer + path.sidebarName = "Heap Viewer"; AddSidebarEntry("Dev Tools", path.sidebarName, 1); - AddWidget(path, "Popout Gfx Debugger", WIDGET_WINDOW_BUTTON) - .CVar(CVAR_WINDOW("SohGfxDebugger")) - .WindowName("GfxDebugger##SoH") + AddWidget(path, "Popout Heap Viewer", WIDGET_WINDOW_BUTTON) + .CVar(CVAR_WINDOW("HeapViewer")) + .WindowName("Heap Viewer") .HideInSearch(true) - .Options(WindowButtonOptions().Tooltip("Enables the separate Gfx Debugger Window.")); + .Options(WindowButtonOptions().Tooltip("Enables the separate Heap Viewer Window.")); } } // namespace SohGui diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index fc4f9f95c80..2d0b15a94bf 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -1193,7 +1193,7 @@ void SohMenu::AddMenuEnhancements() { AddWidget(path, "N64 Weird Frames", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("N64WeirdFrames")) .Options(CheckboxOptions().Tooltip( - "Restores N64 Weird Frames allowing weirdshots and weirdslides to behave the same as N64.")); + "Restores N64 Weird Frames allowing weirdshots and weirdslides to behave the same as N64.")); AddWidget(path, "Bombchus Out of Bounds", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("BombchusOOB")) .Options( @@ -1212,6 +1212,11 @@ void SohMenu::AddMenuEnhancements() { .Options(CheckboxOptions().Tooltip( "Restores a bug from NTSC 1.0/1.1 that allows you to obtain the eyeball frog from King Zora " "instead of the Zora Tunic by Holding Shield.")); + AddWidget(path, "Hardware Memory Limits", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("HardwareMemoryLimits")) + .Options(CheckboxOptions().Tooltip( + "Restores the memory limits from original hardware. Actors will fail to spawn after repeated room " + "transitions, allowing heap manipulation techniques.")); AddWidget(path, "Misc Restorations", WIDGET_SEPARATOR_TEXT); AddWidget(path, "Fix L&Z Page Switch in Pause Menu", WIDGET_CVAR_CHECKBOX) diff --git a/soh/soh/z_scene_otr.cpp b/soh/soh/z_scene_otr.cpp index 2ac7b8e3839..c856e3042e6 100644 --- a/soh/soh/z_scene_otr.cpp +++ b/soh/soh/z_scene_otr.cpp @@ -1,4 +1,5 @@ #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #include "ResourceManagerHelpers.h" #include #include "soh/resource/type/Scene.h" @@ -105,6 +106,10 @@ bool Scene_CommandSpecialFiles(PlayState* play, SOH::ISceneCommand* cmd) { play->objectCtx.subKeepIndex = Object_Spawn(&play->objectCtx, specialCmd->specialObjects.globalObject); } + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_StoreElfMsgNum(specialCmd->specialObjects.elfMessage); + // #endregion + if (specialCmd->specialObjects.elfMessage != 0) { auto res = (Ship::Blob*)OTRPlay_LoadFile(play, sNaviMsgFiles[specialCmd->specialObjects.elfMessage - 1].fileName); diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index ef249ae9e83..4161d95fa01 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -14,6 +14,7 @@ #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/nametag.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #include "soh/ActorDB.h" #include "soh/OTRGlobals.h" @@ -2596,6 +2597,11 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { } play->numSetupActors = 0; GameInteractor_ExecuteOnSceneSpawnActors(); + + // #region SOH [Enhancement] - Hardware Memory Limits + // Log shadow heap stats after each room transition for hardware validation. + N64Mem_BenchmarkTransition(play); + // #endregion } if (actorCtx->unk_02 != 0) { @@ -2654,7 +2660,26 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { CollisionCheck_ResetDamage(&actor->colChkInfo); actor = actor->next; } else if (actor->update == NULL) { - if (!actor->isDrawn) { + // #region SOH [Enhancement] - Hardware Memory Limits + // + // On original hardware, isDrawn reflects N64's draw distance from the previous frame. SoH's extended + // draw distance (Ship_CalcShouldDrawAndUpdate) sets isDrawn more permissively, so actors that N64 + // would consider off-screen appear drawn on SoH. + // + // Recalculate the draw decision using N64's culling check (func_800314D4) so the immediate vs. + // deferred free path (ergo, heap coalescing pattern) matches original hardware. + if (N64Mem_IsActive()) { + const bool n64WouldDraw = actor->init == NULL && actor->draw != NULL && + (actor->flags & ACTOR_FLAG_DRAW_CULLING_DISABLED || + func_800314D4(play, actor, &actor->projectedPos, actor->projectedW)); + if (!n64WouldDraw) { + actor = Actor_Delete(&play->actorCtx, actor, play); + } else { + Actor_Destroy(actor, play); + actor = actor->next; + } + } else if (!actor->isDrawn) { + // #endregion actor = Actor_Delete(&play->actorCtx, actor, play); } else { Actor_Destroy(actor, play); @@ -2686,7 +2711,26 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { actor->colorFilterTimer--; } if (GameInteractor_ShouldActorUpdate(actor)) { + // #region SOH [Enhancement] - Hardware Memory Limits + // + // On N64, the original actor would be running here and its children would be different -- we + // already charged the original's overlay and instance sizes, so the replacement children are + // excess. + // + // If the actor is a randomized replacement, suppress shadow allocations for any children it + // spawns during its update (Actor_Spawn, Actor_SpawnAsChild, subsidiaries). + const s32 n64MemSavedUpdate = N64Mem_GetRandomizedInit(); + if (N64Mem_IsRandomizedActor(actor)) { + N64Mem_SetRandomizedInit(1); + } + // #endregion + actor->update(actor, play); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_SetRandomizedInit(n64MemSavedUpdate); + // #endregion + GameInteractor_ExecuteOnActorUpdate(actor); } func_8003F8EC(play, &play->colCtx.dyna, actor); @@ -3193,7 +3237,27 @@ void func_80031B14(PlayState* play, ActorContext* actorCtx) { while (actor != NULL) { if ((actor->room >= 0) && (actor->room != play->roomCtx.curRoom.num) && (actor->room != play->roomCtx.prevRoom.num)) { - if (!actor->isDrawn) { + // #region SOH [Enhancement] - Hardware Memory Limits + // + // On original hardware, isDrawn reflects N64's draw distance from the previous frame. SoH's extended + // draw distance (Ship_CalcShouldDrawAndUpdate) sets isDrawn more permissively, so actors that N64 + // would consider off-screen appear drawn on SoH. + // + // Recalculate the draw decision using N64's culling check (func_800314D4) so the immediate vs. + // deferred free path (ergo, heap coalescing pattern) matches original hardware. + if (N64Mem_IsActive()) { + const bool n64WouldDraw = actor->init == NULL && actor->draw != NULL && + (actor->flags & ACTOR_FLAG_DRAW_CULLING_DISABLED || + func_800314D4(play, actor, &actor->projectedPos, actor->projectedW)); + if (!n64WouldDraw) { + actor = Actor_Delete(actorCtx, actor, play); + } else { + Actor_Kill(actor); + Actor_Destroy(actor, play); + actor = actor->next; + } + } else if (!actor->isDrawn) { + // #endregion actor = Actor_Delete(actorCtx, actor, play); } else { Actor_Kill(actor); @@ -3323,6 +3387,12 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos s32 objBankIndex; u32 temp; + // #region SOH [Enhancement] - Hardware Memory Limits + // Save the randomizer state before spawn. Enemy Randomizer may swap the actor ID, so the shadow needs to know + // both the original and randomized IDs to allocate the correct sizes. + const s32 n64MemSavedInit = N64Mem_GetRandomizedInit(); + // #endregion + ActorDBEntry* dbEntry = ActorDB_Retrieve(actorId); assert(dbEntry->valid); @@ -3353,6 +3423,16 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos return NULL; } + // #region SOH [Enhancement] - Hardware Memory Limits + // Shadow-allocate the actor's overlay code. If the shadow heap is full, block the spawn. + if (dbEntry->numLoaded == 0) { + if (!N64Mem_AllocOverlay(actorId, dbEntry->allocType)) { + Actor_FreeOverlay(dbEntry); + return NULL; + } + } + // #endregion + actor = ZELDA_ARENA_MALLOC_DEBUG(dbEntry->instanceSize); if (actor == NULL) { @@ -3363,6 +3443,15 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos return NULL; } + // #region SOH [Enhancement] - Hardware Memory Limits + // Shadow-allocate the actor's instance struct. If the shadow heap is full, block the spawn. + if (!N64Mem_AllocInstance(actorId, params, actor)) { + ZELDA_ARENA_FREE_DEBUG(actor); + Actor_FreeOverlay(dbEntry); + return NULL; + } + // #endregion + // #region SOH [ObjectExtension] SetActorListIndex(actor, -1); // #endregion @@ -3406,6 +3495,12 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos Actor_Init(actor, play); gSegments[6] = temp; + // #region SOH [Enhancement] - Hardware Memory Limits + // Restore randomizer state after the actor's Init has run. + N64Mem_ClearOriginalActorId(); + N64Mem_SetRandomizedInit(n64MemSavedInit); + // #endregion + GameInteractor_ExecuteOnActorSpawn(actor); return actor; @@ -3523,9 +3618,24 @@ Actor* Actor_Delete(ActorContext* actorCtx, Actor* actor, PlayState* play) { ObjectExtension_Free(actor); // #endregion + // #region SOH [Enhancement] - Hardware Memory Limits + // Shadow-free the actor instance. Returns the original (pre-randomizer) actor ID so the overlay free uses the + // correct ID even when Enemy Randomizer swapped it at spawn time. + const s16 n64MemOriginalId = N64Mem_FreeInstance(actor); + const s16 n64MemActorId = n64MemOriginalId >= 0 ? n64MemOriginalId : actor->id; + // #endregion + ZELDA_ARENA_FREE_DEBUG(actor); dbEntry->numLoaded--; + + // #region SOH [Enhancement] - Hardware Memory Limits + // Shadow-free the overlay code when the last instance using it is deleted. + if (dbEntry->numLoaded == 0) { + N64Mem_FreeOverlay(n64MemActorId, dbEntry->allocType); + } + // #endregion + Actor_FreeOverlay(dbEntry); return newHead; @@ -6485,4 +6595,4 @@ s32 func_80038290(PlayState* play, Actor* actor, Vec3s* arg2, Vec3s* arg3, Vec3f func_80037FC8(actor, &sp24, arg2, arg3); return true; -} +} \ No newline at end of file diff --git a/soh/src/code/z_camera.c b/soh/src/code/z_camera.c index 0d30dcb2e60..272669ada5a 100644 --- a/soh/src/code/z_camera.c +++ b/soh/src/code/z_camera.c @@ -8,6 +8,7 @@ #include "soh/frame_interpolation.h" #include "soh/Enhancements/controls/Mouse.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" s16 Camera_ChangeSettingFlags(Camera* camera, s16 setting, s16 flags); s32 Camera_ChangeModeFlags(Camera* camera, s16 mode, u8 flags); @@ -6943,6 +6944,14 @@ Camera* Camera_Create(View* view, CollisionContext* colCtx, PlayState* play) { Camera* newCamera = ZELDA_ARENA_MALLOC_DEBUG(sizeof(*newCamera)); if (newCamera != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + // Cameras are ZeldaArena allocations on original hardware. Track in the shadow heap. + if (!N64Mem_AllocSubsidiary(newCamera, N64_SIZEOF_CAMERA)) { + ZELDA_ARENA_FREE_DEBUG(newCamera); + return NULL; + } + // #endregion + osSyncPrintf(VT_FGCOL(BLUE) "camera: create --- allocate %d byte" VT_RST "\n", sizeof(*newCamera) * 4); Camera_Init(newCamera, view, colCtx, play); } else { @@ -6954,6 +6963,10 @@ Camera* Camera_Create(View* view, CollisionContext* colCtx, PlayState* play) { void Camera_Destroy(Camera* camera) { if (camera != NULL) { osSyncPrintf(VT_FGCOL(BLUE) "camera: destroy ---" VT_RST "\n"); + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(camera); + // #endregion + ZELDA_ARENA_FREE_DEBUG(camera); } else { osSyncPrintf(VT_COL(YELLOW, BLACK) "camera: destroy: already cleared\n" VT_RST); diff --git a/soh/src/code/z_collision_check.c b/soh/src/code/z_collision_check.c index aa466e412eb..baccb127d5c 100644 --- a/soh/src/code/z_collision_check.c +++ b/soh/src/code/z_collision_check.c @@ -2,6 +2,7 @@ #include "vt.h" #include "overlays/effects/ovl_Effect_Ss_HitMark/z_eff_ss_hitmark.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #include typedef s32 (*ColChkResetFunc)(PlayState*, Collider*); @@ -337,6 +338,11 @@ s32 Collider_FreeJntSph(PlayState* play, ColliderJntSph* collider) { collider->count = 0; if (collider->elements != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + // Collider element arrays are ZeldaArena allocations. Track frees in the shadow heap. + N64Mem_FreeSubsidiary(collider->elements); + // #endregion + ZELDA_ARENA_FREE_DEBUG(collider->elements); } collider->elements = NULL; @@ -378,6 +384,15 @@ s32 Collider_SetJntSphToActor(PlayState* play, ColliderJntSph* dest, ColliderJnt return 0; } + // #region SOH [Enhancement] - Hardware Memory Limits + if (!N64Mem_AllocSubsidiary(dest->elements, src->count * N64_SIZEOF_COLLIDER_JNT_SPH_ELEM)) { + ZELDA_ARENA_FREE_DEBUG(dest->elements); + dest->elements = NULL; + dest->count = 0; + return 0; + } + // #endregion + for (destElem = dest->elements, srcElem = src->elements; destElem < dest->elements + dest->count; destElem++, srcElem++) { Collider_InitJntSphElement(play, destElem); @@ -406,6 +421,15 @@ s32 Collider_SetJntSphAllocType1(PlayState* play, ColliderJntSph* dest, Actor* a return 0; } + // #region SOH [Enhancement] - Hardware Memory Limits + if (!N64Mem_AllocSubsidiary(dest->elements, src->count * N64_SIZEOF_COLLIDER_JNT_SPH_ELEM)) { + ZELDA_ARENA_FREE_DEBUG(dest->elements); + dest->elements = NULL; + dest->count = 0; + return 0; + } + // #endregion + for (destElem = dest->elements, srcElem = src->elements; destElem < dest->elements + dest->count; destElem++, srcElem++) { Collider_InitJntSphElement(play, destElem); @@ -433,6 +457,16 @@ s32 Collider_SetJntSphAlloc(PlayState* play, ColliderJntSph* dest, Actor* actor, osSyncPrintf(VT_RST); return 0; } + + // #region SOH [Enhancement] - Hardware Memory Limits + if (!N64Mem_AllocSubsidiary(dest->elements, src->count * N64_SIZEOF_COLLIDER_JNT_SPH_ELEM)) { + ZELDA_ARENA_FREE_DEBUG(dest->elements); + dest->elements = NULL; + dest->count = 0; + return 0; + } + // #endregion + for (destElem = dest->elements, srcElem = src->elements; destElem < dest->elements + dest->count; destElem++, srcElem++) { Collider_InitJntSphElement(play, destElem); @@ -700,6 +734,10 @@ s32 Collider_FreeTris(PlayState* play, ColliderTris* tris) { tris->count = 0; if (tris->elements != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(tris->elements); + // #endregion + ZELDA_ARENA_FREE_DEBUG(tris->elements); } tris->elements = NULL; @@ -740,6 +778,16 @@ s32 Collider_SetTrisAllocType1(PlayState* play, ColliderTris* dest, Actor* actor osSyncPrintf(VT_RST); return 0; } + + // #region SOH [Enhancement] - Hardware Memory Limits + if (!N64Mem_AllocSubsidiary(dest->elements, src->count * N64_SIZEOF_COLLIDER_TRIS_ELEM)) { + ZELDA_ARENA_FREE_DEBUG(dest->elements); + dest->elements = NULL; + dest->count = 0; + return 0; + } + // #endregion + for (destElem = dest->elements, srcElem = src->elements; destElem < dest->elements + dest->count; destElem++, srcElem++) { Collider_InitTrisElement(play, destElem); @@ -768,6 +816,15 @@ s32 Collider_SetTrisAlloc(PlayState* play, ColliderTris* dest, Actor* actor, Col return 0; } + // #region SOH [Enhancement] - Hardware Memory Limits + if (!N64Mem_AllocSubsidiary(dest->elements, src->count * N64_SIZEOF_COLLIDER_TRIS_ELEM)) { + ZELDA_ARENA_FREE_DEBUG(dest->elements); + dest->elements = NULL; + dest->count = 0; + return 0; + } + // #endregion + for (destElem = dest->elements, srcElem = src->elements; destElem < dest->elements + dest->count; destElem++, srcElem++) { Collider_InitTrisElement(play, destElem); diff --git a/soh/src/code/z_effect_soft_sprite.c b/soh/src/code/z_effect_soft_sprite.c index 06263304cb3..a097e476a49 100644 --- a/soh/src/code/z_effect_soft_sprite.c +++ b/soh/src/code/z_effect_soft_sprite.c @@ -2,6 +2,7 @@ #include "vt.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #include EffectSsInfo sEffectSsInfo = { 0 }; // "EffectSS2Info" @@ -183,6 +184,14 @@ void EffectSs_Spawn(PlayState* play, s32 type, s32 priority, void* initParams) { return; } + // #region SOH [Enhancement] - Hardware Memory Limits + // Effect overlays are MallocR'd (allocated from the top of the heap) on original hardware. Track in the shadow + // heap so the forward/reverse alloc boundary stays accurate. + if (!N64Mem_AllocEffectOverlay(type)) { + return; + } + // #endregion + sEffectSsInfo.searchStartIndex = index + 1; overlaySize = (uintptr_t)overlayEntry->vramEnd - (uintptr_t)overlayEntry->vramStart; diff --git a/soh/src/code/z_malloc.c b/soh/src/code/z_malloc.c index 26d2fb855ff..9f5bfbc6d69 100644 --- a/soh/src/code/z_malloc.c +++ b/soh/src/code/z_malloc.c @@ -91,6 +91,13 @@ void ZeldaArena_GetSizes(u32* outMaxFree, u32* outFree, u32* outAlloc) { ArenaImpl_GetSizes(&sZeldaArena, outMaxFree, outFree, outAlloc); } +// #region SOH [Enhancement] - Heap Viewer +ArenaNode* ZeldaArena_GetHead() +{ + return sZeldaArena.head; +} +// #endregion + void ZeldaArena_Check() { __osCheckArena(&sZeldaArena); } diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index f73d076de42..a79ee0bc7ce 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -9,6 +9,7 @@ #include #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #include "soh/OTRGlobals.h" #include "soh/ResourceManagerHelpers.h" #include "soh/SaveManager.h" @@ -19,6 +20,7 @@ #include #include + TransitionUnk sTrnsnUnk; s32 gTrnsnUnkState; VisMono gPlayVisMono; @@ -402,10 +404,19 @@ void Play_Init(GameState* thisx) { SystemArena_Display(); - // OTRTODO allocate double the normal amount of memory - // This is to avoid some parts of the game, like loading actors, causing OoM - // This is potionally unavoidable due to struct size differences, but is x2 the right amount? - GameState_Realloc(&play->state, 0x1D4790 * 2); + // #region SOH [Enhancement] - Hardware Memory Limits + // SoH doubles the THA budget to avoid OoM from struct size differences. When Hardware Memory Limits is active, + // use the original N64 budget so the ZeldaArena is the correct size. + if (CVarGetInteger(CVAR_ENHANCEMENT("HardwareMemoryLimits"), 0)) { + GameState_Realloc(&play->state, 0x1D4790); + } else { + // #endregion + // OTRTODO allocate double the normal amount of memory + // This is to avoid some parts of the game, like loading actors, causing OoM + // This is potionally unavoidable due to struct size differences, but is x2 the right amount? + GameState_Realloc(&play->state, 0x1D4790 * 2); + } + KaleidoManager_Init(play); View_Init(&play->view, gfxCtx); Audio_SetExtraFilter(0); @@ -568,6 +579,13 @@ void Play_Init(GameState* thisx) { osSyncPrintf("ZELDA ALLOC SIZE=%x\n", THA_GetSize(&play->state.tha)); zAllocSize = THA_GetSize(&play->state.tha); + + // #region SOH [Enhancement] - Hardware Memory Limits + // Capture the ZeldaArena size (THA remainder) before it's consumed. The shadow arena uses this to compute its own + // size, matching the original hardware's available heap space. + N64Mem_StoreThaRemainder(zAllocSize); + // #endregion + zAlloc = (uintptr_t)GAMESTATE_ALLOC_MC(&play->state, zAllocSize); zAllocAligned = (zAlloc + 8) & ~0xF; ZeldaArena_Init((void*)zAllocAligned, zAllocSize - (zAllocAligned - zAlloc)); @@ -575,6 +593,12 @@ void Play_Init(GameState* thisx) { osSyncPrintf("ゼルダヒープ %08x-%08x\n", zAllocAligned, (u8*)zAllocAligned + zAllocSize - (s32)(zAllocAligned - zAlloc)); + // #region SOH [Enhancement] - Hardware Memory Limits + // Create (or recreate) the shadow arena for this scene. The shadow mirrors every ZeldaArena allocation at + // original hardware sizes, tracking fragmentation independently of SoH's heap. + N64Mem_Reset(play); + // #endregion + Fault_AddClient(&D_801614B8, ZeldaArena_Display, NULL, NULL); // In order to keep masks equipped on first load, we need to pre-set the age reqs for the item and slot diff --git a/soh/src/code/z_skelanime.c b/soh/src/code/z_skelanime.c index 3449c2ed91a..3b89c2199b9 100644 --- a/soh/src/code/z_skelanime.c +++ b/soh/src/code/z_skelanime.c @@ -5,6 +5,7 @@ #include #include "soh/ResourceManagerHelpers.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" #define ANIM_INTERP 1 @@ -1128,7 +1129,16 @@ void SkelAnime_InitLink(PlayState* play, SkelAnime* skelAnime, FlexSkeletonHeade if (jointTable == NULL) { skelAnime->jointTable = ZELDA_ARENA_MALLOC_DEBUG(allocSize); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->jointTable, allocSize); + // #endregion + skelAnime->morphTable = ZELDA_ARENA_MALLOC_DEBUG(allocSize); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->morphTable, allocSize); + // #endregion } else { assert(limbBufCount == limbCount); @@ -1457,7 +1467,16 @@ s32 SkelAnime_Init(PlayState* play, SkelAnime* skelAnime, SkeletonHeader* skelet skelAnime->skeleton = SEGMENTED_TO_VIRTUAL(skeletonHeader->segment); if (jointTable == NULL) { skelAnime->jointTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->jointTable, skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + // #endregion + skelAnime->morphTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->morphTable, skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + // #endregion } else { assert(limbCount == skelAnime->limbCount); skelAnime->jointTable = jointTable; @@ -1492,7 +1511,15 @@ s32 SkelAnime_InitFlex(PlayState* play, SkelAnime* skelAnime, FlexSkeletonHeader if (jointTable == NULL) { skelAnime->jointTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->jointTable, skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + // #endregion + skelAnime->morphTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->morphTable, skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + // #endregion } else { assert(limbCount == skelAnime->limbCount); skelAnime->jointTable = jointTable; @@ -1524,7 +1551,17 @@ s32 SkelAnime_InitSkin(PlayState* play, SkelAnime* skelAnime, SkeletonHeader* sk skelAnime->limbCount = skeletonHeader->limbCount + 1; skelAnime->skeleton = SEGMENTED_TO_VIRTUAL(skeletonHeader->segment); skelAnime->jointTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->jointTable, skelAnime->limbCount * sizeof(*skelAnime->jointTable)); + // #endregion + skelAnime->morphTable = ZELDA_ARENA_MALLOC_DEBUG(skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skelAnime->morphTable, skelAnime->limbCount * sizeof(*skelAnime->morphTable)); + // #endregion + if ((skelAnime->jointTable == NULL) || (skelAnime->morphTable == NULL)) { osSyncPrintf(VT_FGCOL(RED)); // "Memory allocation error" @@ -1927,12 +1964,20 @@ s32 Animation_OnFrame(SkelAnime* skelAnime, f32 frame) { */ void SkelAnime_Free(SkelAnime* skelAnime, PlayState* play) { if (skelAnime->jointTable != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(skelAnime->jointTable); + // #endregion + ZELDA_ARENA_FREE_DEBUG(skelAnime->jointTable); } else { osSyncPrintf("now_joint あきまへん!!\n"); // "now_joint is freed! !" } if (skelAnime->morphTable != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(skelAnime->morphTable); + // #endregion + ZELDA_ARENA_FREE_DEBUG(skelAnime->morphTable); } else { osSyncPrintf("morf_joint あきまへん!!\n"); // "morf_joint is freed !!" diff --git a/soh/src/code/z_skin_awb.c b/soh/src/code/z_skin_awb.c index 504b6376d41..00842e4cb16 100644 --- a/soh/src/code/z_skin_awb.c +++ b/soh/src/code/z_skin_awb.c @@ -2,6 +2,7 @@ #include "overlays/actors/ovl_En_fHG/z_en_fhg.h" #include #include "soh/ResourceManagerHelpers.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" /** * Initialises the Vtx buffers used for limb at index `limbIndex` @@ -57,6 +58,10 @@ void Skin_Init(PlayState* play, Skin* skin, SkeletonHeader* skeletonHeader, Anim assert(skin->vtxTable != NULL); + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(skin->vtxTable, limbCount * N64_SIZEOF_SKIN_LIMB_VTX); + // #endregion + for (i = 0; i < limbCount; i++) { SkinLimbVtx* vtxEntry = &skin->vtxTable[i]; SkinLimb* limb = SEGMENTED_TO_VIRTUAL(skeleton[i]); @@ -74,9 +79,17 @@ void Skin_Init(PlayState* play, Skin* skin, SkeletonHeader* skeletonHeader, Anim vtxEntry->buf[0] = ZELDA_ARENA_MALLOC_DEBUG(animatedLimbData->totalVtxCount * sizeof(Vtx)); assert(vtxEntry->buf[0] != NULL); + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(vtxEntry->buf[0], animatedLimbData->totalVtxCount * sizeof(Vtx)); + // #endregion + vtxEntry->buf[1] = ZELDA_ARENA_MALLOC_DEBUG(animatedLimbData->totalVtxCount * sizeof(Vtx)); assert(vtxEntry->buf[1] != NULL); + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_AllocSubsidiary(vtxEntry->buf[1], animatedLimbData->totalVtxCount * sizeof(Vtx)); + // #endregion + Skin_InitAnimatedLimb(play, skin, i); } } @@ -93,16 +106,28 @@ void Skin_Free(PlayState* play, Skin* skin) { for (i = 0; i < skin->limbCount; i++) { if (skin->vtxTable[i].buf[0] != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(skin->vtxTable[i].buf[0]); + // #endregion + ZELDA_ARENA_FREE_DEBUG(skin->vtxTable[i].buf[0]); skin->vtxTable[i].buf[0] = NULL; } if (skin->vtxTable[i].buf[1] != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(skin->vtxTable[i].buf[1]); + // #endregion + ZELDA_ARENA_FREE_DEBUG(skin->vtxTable[i].buf[1]); skin->vtxTable[i].buf[1] = NULL; } } if (skin->vtxTable != NULL) { + // #region SOH [Enhancement] - Hardware Memory Limits + N64Mem_FreeSubsidiary(skin->vtxTable); + // #endregion + ZELDA_ARENA_FREE_DEBUG(skin->vtxTable); } diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 7ffe8af7f43..f4bb1b89032 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -29,6 +29,8 @@ #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/randomizer/randomizer_grotto.h" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/HardwareMemoryLimits.hpp" +#include "soh/Enhancements/Restorations/HardwareMemoryLimits/N64SizeData.hpp" #include "soh/frame_interpolation.h" #include "soh/OTRGlobals.h" #include "soh/ResourceManagerHelpers.h" @@ -37,6 +39,7 @@ #include #include + // Some player animations are played at this reduced speed, for reasons yet unclear. // This is called "adjusted" for now. #define PLAYER_ANIM_ADJUSTED_SPEED (2.0f / 3.0f) @@ -10844,7 +10847,14 @@ void Player_Init(Actor* thisx, PlayState* play2) { // `giObjectSegment` is used for both "get item" objects and title cards. The maximum size for // get item objects is 0x2000 (see the assert in func_8083AE40), and the maximum size for // title cards is 0x1000 * LANGUAGE_MAX since each title card image includes all languages. - this->giObjectSegment = (void*)(((uintptr_t)ZELDA_ARENA_MALLOC_DEBUG(0x3008) + 8) & ~0xF); + + // #region SOH [Enhancement] - Hardware Memory Limits + // The GI objct segment holds the title card and get-item data. On original hardware, this is a 0x3008-byte + // ZeldaArena allocation regardless of region. Track it in the shadow. + void* giRaw = ZELDA_ARENA_MALLOC_DEBUG(0x3008); + N64Mem_AllocSubsidiary(giRaw, 0x3008); + this->giObjectSegment = (void*)((uintptr_t)giRaw + 8 & ~0xF); + // #endregion respawnFlag = gSaveContext.respawnFlag;