diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..7afed40d3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,143 @@ +# AGENTS.md - Agent Runbook (BFBB) + +This file is the runbook for automated contributions to **BFBB**. + +Goal: make steady, source-plausible match progress in this repo and keep that work on one long-lived PR branch instead of spinning up a new PR for every small improvement. When driven by `agentic_loop.py`, keep repeating that loop indefinitely while there are credible code/data improvements to make. + +## Project assumptions + +- Repo root is the current checkout. +- The active version is `GQPE78`. +- Required local asset: `orig/GQPE78/sys/main.dol`. +- The useful automation artifacts for this project are: + - `build/GQPE78/report.json` + - `build/GQPE78/progress.json` + - `objdiff.json` +- Do not depend on old `.MAP` files, symbol extractors, or state files copied over from another repo. + +## Setup + +Only run setup steps if the build artifacts are missing or stale. + +1. Configure the project: + + ```sh + python configure.py + ``` + +2. Build the project: + + ```sh + ninja + ``` + +Success criteria: +- `build/GQPE78/report.json` exists +- `build/GQPE78/main.dol: OK` + +## Contribution loop + +### 1) Branch and PR management + +- If you are already on a non-`main` branch, treat it as the active long-lived automation branch. Stay on it, commit to it, and push back to it. +- If you are on `main` and the worktree is clean, update `main` and create one long-lived branch for the automation run, for example: + + ```sh + git pull origin main + git checkout -b pr/mega/$(date -u +%s) + ``` + +- Create at most one PR for that branch. +- Once the PR exists, keep pushing more commits to the same branch and updating the same PR. +- Do not create a new PR for each unit or function. +- Do not branch-hop during the loop. If a usable non-`main` branch is already checked out, keep iterating there. +- Do not assume local uncommitted changes are disposable. If unrelated user work is present, leave it alone. + +### 2) Pick the next target + +Run: + +```sh +python tools/agent_select_target.py +``` + +The selector's default mode is quality-first near-match polishing: +- it reads `build/GQPE78/report.json` +- it ranks incomplete units by balanced progress across fuzzy, code, data, and function match metrics +- it prefers near-complete units with real source paths over source-less or badly imbalanced targets +- it prints a short list of candidate units and a few weak functions + +Prefer polishing near-match units first unless there is a concrete reason to chase a lower-match file. Only drop into lower-match exploration when the near-match queue is blocked or exhausted. + +### 3) Edit source + +Work in `src/` and `include/`. + +Priorities: +- recover plausible original source +- improve real code/data matching +- clean up decompiler output into readable C/C++ +- define proper structs, enums, types, and linkage +- use real member access instead of pointer math +- prefer fixes that improve source plausibility even when they are not the fastest way to move a score + +Avoid: +- hardcoded offsets where a typed field belongs in a struct +- extern hacks added only to move a score +- junk comments, debug notes, or commented-out code +- contrived compiler-coaxing that does not look like plausible original source +- trading readable, typed, source-plausible code for a hacked 100% result + +Follow the repo conventions in `CONTRIBUTING.md`. + +### 4) Verify + +Build after edits: + +```sh +ninja +``` + +Check the result with: +- `build/GQPE78/report.json` +- objdiff, if needed + +If the local objdiff CLI exists, a typical command is: + +```sh +build/tools/objdiff-cli.exe diff -p . -u -o - +``` + +Treat real code and data improvements as the signal. Formatting-only churn is not success. A clean, source-plausible near-match is better than an implausible perfect match. + +### 5) Commit and update the PR + +- Commit meaningful improvements on the current active branch. +- Push the current branch after each meaningful improvement. +- If no PR exists yet and the branch has real progress, create one. +- If a PR already exists for the branch, update that PR instead of opening another one. +- Keep the PR description cumulative as the branch grows. + +### 6) Repeat + +Stay on the same branch and repeat the loop until the PR has a worthwhile set of high-quality improvements. After each successful push, go back to target selection and continue iterating. + +## Quality bar + +- Build must pass with `ninja`. +- Progress must be real in report/objdiff output. +- Source should look like plausible original game code. +- Prefer definitions, types, and readable control flow over temporary score hacks. +- A clean 99.99% is better than a hacked or implausible 100%. +- Never revert unrelated user changes. +- Never use destructive git cleanup unless explicitly requested. + +## Quick checklist + +Before pushing: + +1. `ninja` passes. +2. `build/GQPE78/report.json` or objdiff shows real improvement. +3. The code is clean and follows project conventions. +4. The changes stay on the current long-lived PR branch. +5. No new one-off PR was created for this iteration. diff --git a/agentic_loop.py b/agentic_loop.py new file mode 100644 index 000000000..346284005 --- /dev/null +++ b/agentic_loop.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import argparse +import os +import signal +import shutil +import subprocess +import sys +from pathlib import Path + +DEFAULT_TIMEOUT_SECONDS = 25 * 60 + + +def get_repo_root() -> Path: + return Path(__file__).resolve().parent + + +def get_current_branch(repo_root: Path) -> str: + result = subprocess.run( + ["git", "branch", "--show-current"], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + branch = result.stdout.strip() + return branch or "DETACHED" + + +def build_prompt(branch: str) -> str: + if branch not in {"main", "master", "DETACHED"}: + branch_note = ( + f"The current branch is '{branch}'. Treat it as the existing long-lived mega-PR branch " + "and keep iterating on it. Commit to it, push to it, and do not create or switch to another branch." + ) + else: + branch_note = ( + f"The current branch is '{branch}'. If the worktree is clean, create one long-lived PR " + "branch for this automation run and keep reusing it for every later commit." + ) + + return ( + "Follow the instructions in AGENTS.md in this repo. Never ask the user for input. " + f"{branch_note} " + "Use tools/agent_select_target.py to choose the next target from build/GQPE78/report.json. " + "Prefer the selector's default quality-first near-match ranking. " + "Prioritize balanced near-matches that are close across fuzzy, code, data, and function metrics. " + "Focus on plausible BFBB source and real match improvements. " + "Prefer typed, readable, source-plausible code over contrived score hacks; a clean near-match is better than a hacked 100%. " + "Do not create tiny one-off PRs. If a PR already exists for the branch, keep updating that same PR. " + "After each meaningful improvement, build, verify, commit, push the active branch, and continue iterating." + ) + + +def resolve_codex_command() -> list[str]: + if os.name == "nt": + candidates = ("codex.exe", "codex.cmd", "codex.bat", "codex") + else: + candidates = ("codex",) + + for candidate in candidates: + resolved = shutil.which(candidate) + if resolved: + return [resolved] + + raise FileNotFoundError( + "Could not find the Codex CLI in PATH. Install Codex or add its executable location to PATH." + ) + + +def terminate_process_tree(proc: subprocess.Popen[bytes]) -> None: + if os.name == "nt": + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(proc.pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return + + try: + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + except ProcessLookupError: + pass + + +def run_once(repo_root: Path, timeout_seconds: int) -> int: + prompt = build_prompt(get_current_branch(repo_root)) + codex_command = resolve_codex_command() + popen_kwargs = { + "cwd": repo_root, + } + + if os.name == "nt": + popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + popen_kwargs["start_new_session"] = True + + proc = subprocess.Popen(codex_command + ["exec", "--yolo", prompt], **popen_kwargs) + + try: + return proc.wait(timeout=timeout_seconds) + except subprocess.TimeoutExpired: + terminate_process_tree(proc) + return 124 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run Codex in a persistent BFBB automation loop.") + parser.add_argument( + "--timeout", + type=int, + default=DEFAULT_TIMEOUT_SECONDS, + help=f"per-iteration timeout in seconds (default: {DEFAULT_TIMEOUT_SECONDS})", + ) + parser.add_argument( + "--once", + action="store_true", + help="run a single Codex iteration instead of looping forever", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + repo_root = get_repo_root() + + while True: + exit_code = run_once(repo_root, args.timeout) + if args.once: + return exit_code + + if exit_code not in {0, 124}: + print(f"codex exited with status {exit_code}", file=sys.stderr) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/PowerPC_EABI_Support/src/Runtime/runtime.c b/src/PowerPC_EABI_Support/src/Runtime/runtime.c index 94da236b7..eb61686e8 100644 --- a/src/PowerPC_EABI_Support/src/Runtime/runtime.c +++ b/src/PowerPC_EABI_Support/src/Runtime/runtime.c @@ -789,6 +789,7 @@ asm void __cvt_ull_dbl(void) asm void __cvt_sll_flt(void) { + nofralloc; stwu r1, -0x10(r1); clrrwi.r5, r3, 31; beq L_802BA62C; @@ -837,7 +838,6 @@ L_802BA6B4:; lfd f1, 0x8(r1); frsp f1, f1; addi r1, r1, 0x10; - frfree; // Build error said to add this blr } @@ -968,4 +968,4 @@ end:; #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/src/SB/Core/gc/ngcrad3d.c b/src/SB/Core/gc/ngcrad3d.c index b891c4d23..85755318d 100644 --- a/src/SB/Core/gc/ngcrad3d.c +++ b/src/SB/Core/gc/ngcrad3d.c @@ -1,6 +1,7 @@ #include "ngcrad3d.h" #include +#include #include "iFMV.h" #include @@ -31,30 +32,57 @@ static void Setup_surface_array() Built_tables = 1; } -// TODO: -// Defining this struct locally because i believe this isnt 100% right. Or if it is right why isnt this in the bink.h or other headers? -// inestigate this - struct RAD3DIMAGE { - int a; - int b; - unsigned int c; - int d; - int e; - void* f; - int g; - int h; + U32 width; + U32 height; + U32 alpha_pixels; + U32 pixel_size; + U32 surface_type; + void* texture_buffer; + U32 texture_size; + GXTexObj texture; }; +HRAD3DIMAGE Open_RAD_3D_image(HRAD3D, U32 width, U32 height, U32 rad3d_surface_format) +{ + RAD3DIMAGE* image; + U32 pixel_info; + + Setup_surface_array(); + + pixel_info = Pixel_info[rad3d_surface_format]; + image = (RAD3DIMAGE*)iFMVmalloc(sizeof(RAD3DIMAGE)); + if (image == NULL) + { + return NULL; + } + + image->width = width; + image->height = height; + image->alpha_pixels = pixel_info >> 31; + image->pixel_size = pixel_info & 0xff; + image->surface_type = rad3d_surface_format; + image->texture_size = + GXGetTexBufferSize((u16)width, (u16)height, D3D_surface_type[rad3d_surface_format], 0, 0); + image->texture_buffer = iFMVmalloc(image->texture_size); + GXInitTexObj(&image->texture, image->texture_buffer, (u16)width, (u16)height, + (GXTexFmt)D3D_surface_type[rad3d_surface_format], (GXTexWrapMode)0, + (GXTexWrapMode)0, (GXBool)0); + GXInitTexObjLOD(&image->texture, (GXTexFilter)1, (GXTexFilter)1, 1.0f, 0.0f, 0.0f, + (GXBool)0, (GXBool)0, (GXAnisotropy)0); + + return image; +} + void Close_RAD_3D_image(struct RAD3DIMAGE* image) { if (image != 0) { - if (image->f != 0) + if (image->texture_buffer != 0) { - iFMVfree(image->f); - image->f = 0; + iFMVfree(image->texture_buffer); + image->texture_buffer = 0; } iFMVfree(image); } @@ -70,35 +98,26 @@ S32 Lock_RAD_3D_image(HRAD3DIMAGE rad_image, void* out_pixel_buffer, U32* out_bu if (out_pixel_buffer != 0) { - *(void**)(out_pixel_buffer) = rad_image->f; + *(void**)(out_pixel_buffer) = rad_image->texture_buffer; } if (out_buffer_pitch != 0) { - *out_buffer_pitch = rad_image->a * rad_image->d; + *out_buffer_pitch = rad_image->width * rad_image->pixel_size; } if (arg3 != 0) { - *arg3 = rad_image->e; + *arg3 = rad_image->surface_type; } return 1; } -static void GXColor4u8(int r3, int r4, int r5, int r6) +void Unlock_RAD_3D_image(HRAD3DIMAGE rad_image) { - int ptr = 0xcc010000; - *((char*)(ptr)-0x8000) = r3; - *((char*)(ptr)-0x8000) = r4; - *((char*)(ptr)-0x8000) = r5; - *((char*)(ptr)-0x8000) = r6; -} - -static void GXPosition3s16(int r3, int r4, int r5) -{ - int ptr = 0xcc010000; - *(short*)((char*)(ptr)-0x8000) = r3; - *(short*)((char*)(ptr)-0x8000) = r4; - *(short*)((char*)(ptr)-0x8000) = r5; + if (rad_image != NULL) + { + DCStoreRange(rad_image->texture_buffer, rad_image->texture_size); + } } diff --git a/src/SB/Core/x/xBehaveMgr.cpp b/src/SB/Core/x/xBehaveMgr.cpp index 7d3abcf20..8816cf054 100644 --- a/src/SB/Core/x/xBehaveMgr.cpp +++ b/src/SB/Core/x/xBehaveMgr.cpp @@ -212,16 +212,14 @@ void xPsyche::Amnesia(S32 i) } } -// Non-matching: Loop/alloc issue S32 xPsyche::IndexInStack(S32 gid) const { + S32 top = this->staktop; S32 da_idx = -1; - for (S32 i = 0; i <= this->staktop; i++) + for (S32 i = 0; i <= top; i++) { - xGoal* tmpgoal = this->goalstak[i]; - - if (gid == tmpgoal->GetID()) + if (gid == this->goalstak[i]->GetID()) { da_idx = i; break; @@ -242,12 +240,12 @@ xGoal* xPsyche::GetCurGoal() const } } -// Non-matching: Loop/alloc issue xGoal* xPsyche::GIDInStack(S32 gid) const { + S32 top = this->staktop; xGoal* da_goal = NULL; - for (S32 i = 0; i <= this->staktop; i++) + for (S32 i = 0; i <= top; i++) { xGoal* tmpgoal = this->goalstak[i]; @@ -1031,15 +1029,21 @@ void xPsyche::TimerClear() this->tmr_stack[0][this->staktop] = 0.0f; } -// Non-matching: Needs an extra branch that does the same `+= dt` void xPsyche::TimerUpdate(F32 dt) { - F32* p; if (this->staktop < 0) { return; } - p = &this->tmr_stack[0][this->staktop]; - *p += dt; + if (this->staktop >= 0) + { + F32* timer = &this->tmr_stack[0][this->staktop]; + *timer += dt; + } + else + { + F32* timer = &this->tmr_stack[0][this->staktop]; + *timer += dt; + } } diff --git a/src/SB/Core/x/xEntMotion.cpp b/src/SB/Core/x/xEntMotion.cpp index a7bada0b9..45c8524bc 100644 --- a/src/SB/Core/x/xEntMotion.cpp +++ b/src/SB/Core/x/xEntMotion.cpp @@ -999,13 +999,16 @@ void xEntMotionDebugExit() dbg_idx = -1; } -// This scheduling is absolutely shambolic void xEntMotionDebugAdd(xEntMotion* motion) { if (dbg_num < dbg_num_allocd) { - dbg_num++; - dbg_xems[dbg_num] = motion; + U16 index = dbg_num; + U16 next = index + 1; + xEntMotion** xems = dbg_xems; + + dbg_num = next; + xems[index] = motion; } } diff --git a/src/SB/Core/x/xstransvc.cpp b/src/SB/Core/x/xstransvc.cpp index 199917f01..6c0dbb4e5 100644 --- a/src/SB/Core/x/xstransvc.cpp +++ b/src/SB/Core/x/xstransvc.cpp @@ -438,14 +438,15 @@ char* xST_xAssetID_HIPFullPath(U32 aid, U32* sceneID) static S32 XST_PreLoadScene(st_STRAN_SCENE* sdata, const char* name) { S32 buf = 0; - st_PACKER_READ_DATA* spkg = - g_pkrf->Init(sdata->userdata, (char*)name, 0x2e, &buf, g_typeHandlers); - sdata->spkg = spkg; + U32 options = 0x2e; + + sdata->spkg = g_pkrf->Init(sdata->userdata, (char*)name, options, &buf, g_typeHandlers); if (sdata->spkg != NULL) { return buf; } - return NULL; + + return 0; } static char* XST_translate_sid(U32 sid, char* extension) @@ -499,13 +500,13 @@ static void XST_unlock(st_STRAN_SCENE* sdata) { if (sdata != NULL) { - if (g_xstdata.loadlock & 1 << sdata->lockid) + st_STRAN_DATA* stran = &g_xstdata; + U32 loadlock = stran->loadlock; + U32 lock = 1 << sdata->lockid; + + if (loadlock & lock) { - // Can't figure out how to get the andc instruction instead of two instructions - // Seems to only generate andc if I remove the memset call. - // NOTE (Square): pulling 1 << sdata->lockid into a temp variable works but - // causes regswaps. - g_xstdata.loadlock &= ~(1 << sdata->lockid); + stran->loadlock = loadlock & ~lock; memset(sdata, 0, sizeof(st_STRAN_SCENE)); } } diff --git a/src/SB/Game/zNPCTypeSubBoss.cpp b/src/SB/Game/zNPCTypeSubBoss.cpp index a74dd0d7e..a24912f24 100644 --- a/src/SB/Game/zNPCTypeSubBoss.cpp +++ b/src/SB/Game/zNPCTypeSubBoss.cpp @@ -32,11 +32,7 @@ char* g_strz_subbanim[ANIM_COUNT] = "LassoGrab01", "LassoHoldLeft01", "LassoHoldRight01", -#if 1 // needed until SUBB_InitEffects is matching - "LassoFree01\0PAREMIT_CLOUD" -#else "LassoFree01" -#endif }; // clang-format on @@ -117,9 +113,9 @@ void zNPCSubBoss::Setup() void SUBB_InitEffects() { - // non-matching: scheduling g_pemit_holder = zParEmitterFind("PAREMIT_CLOUD"); - g_parf_holder.custom_flags = 0x100; + xParEmitterCustomSettings* custom = &g_parf_holder; + custom->custom_flags = 0x100; } zNPCSubBoss::zNPCSubBoss(S32 myType) : zNPCCommon(myType) diff --git a/src/SB/Game/zSurface.cpp b/src/SB/Game/zSurface.cpp index 10f29cf2b..6cf7c2860 100644 --- a/src/SB/Game/zSurface.cpp +++ b/src/SB/Game/zSurface.cpp @@ -120,12 +120,19 @@ static void zSurfaceInitDefaultSurface() void zSurfaceRegisterMapper(U32 assetId) { - if (sMapperCount >= MAX_MAPPER) return; - if (!assetId) return; + zMaterialMapAsset* mapper; - sMapper[sMapperCount] = (zMaterialMapAsset*)xSTFindAsset(assetId, NULL); - if (sMapper[sMapperCount]) { - sMapperCount++; + if (sMapperCount >= MAX_MAPPER) + return; + + if (!assetId) + return; + + mapper = (zMaterialMapAsset*)xSTFindAsset(assetId, NULL); + sMapper[sMapperCount] = mapper; + if (mapper) + { + sMapperCount = sMapperCount + 1; } } diff --git a/src/SB/Game/zTalkBox.cpp b/src/SB/Game/zTalkBox.cpp index 1c063d4dd..d7e046bab 100644 --- a/src/SB/Game/zTalkBox.cpp +++ b/src/SB/Game/zTalkBox.cpp @@ -931,10 +931,14 @@ namespace } static void stop_audio_effect() { - if ((shared.active) && (shared.active->asset->audio_effect != 1)) + if (shared.active) { - zMusicSetVolume(1.0f, music_fade_delay); - return; + switch (shared.active->asset->audio_effect) + { + case 1: + zMusicSetVolume(1.0f, music_fade_delay); + break; + } } } diff --git a/src/SB/Game/zTaskBox.cpp b/src/SB/Game/zTaskBox.cpp index 0c5819344..498b981ad 100644 --- a/src/SB/Game/zTaskBox.cpp +++ b/src/SB/Game/zTaskBox.cpp @@ -80,7 +80,7 @@ void ztaskbox::start_talk(zNPCCommon* npc) ztalkbox* talkbox = (ztalkbox*)zSceneFindObject(asset->talk_box); if (talkbox != NULL) { - U32 text = current->get_text(asset->stages[state]); + const char* text = current->get_text(asset->stages[state]); if (text != 0) { shared = this; @@ -306,7 +306,7 @@ void ztaskbox::on_talk_stop(ztalkbox::answer_enum answer) } } -U32 ztaskbox::get_text(U32 textID) +const char* ztaskbox::get_text(U32 textID) { U32 id = textID; xGroup* group = (xGroup*)zSceneFindObject(textID); @@ -314,7 +314,7 @@ U32 ztaskbox::get_text(U32 textID) { if (group->baseType != eBaseTypeGroup) { - return 0; + return NULL; } id = group->get_any(); @@ -322,20 +322,16 @@ U32 ztaskbox::get_text(U32 textID) if (id == 0) { - return 0; + return NULL; } - // What type is this? - void* asset = xSTFindAsset(id, NULL); + xTextAsset* asset = (xTextAsset*)xSTFindAsset(id, NULL); if (asset == NULL) { - return 0; - } - else - { - // HACK - return (U32)asset + 4; + return NULL; } + + return (const char*)(asset + 1); } S32 ztaskbox::cb_dispatch(xBase*, xBase* to, U32 event, const F32*, xBase*) diff --git a/src/SB/Game/zTaskBox.h b/src/SB/Game/zTaskBox.h index e4cb3ca15..590d81e57 100644 --- a/src/SB/Game/zTaskBox.h +++ b/src/SB/Game/zTaskBox.h @@ -106,7 +106,7 @@ struct ztaskbox : xBase void complete(); static void init(); bool exists(state_enum stage); - static U32 get_text(U32); + static const char* get_text(U32); U32 StatusGet() const; }; diff --git a/src/dolphin/src/os/OS.c b/src/dolphin/src/os/OS.c index 626561042..ea11d272d 100644 --- a/src/dolphin/src/os/OS.c +++ b/src/dolphin/src/os/OS.c @@ -8,7 +8,7 @@ extern OSTime __OSGetSystemTime(); static const char* __OSVersion = "<< Dolphin SDK - OS\trelease build: Apr 17 2003 12:33:06 (0x2301) >>"; // needs to be "<< Dolphin SDK - DSP\trelease build: Apr 17 2003 12:34:16 (0x2301) >>"? -extern char _db_stack_end[]; +extern char _db_stack_end : 0x803D8A50; #define OS_BI2_DEBUG_ADDRESS 0x800000F4 #define DEBUGFLAG_ADDR 0x800030E8 @@ -32,8 +32,8 @@ static __OSExceptionHandler* OSExceptionTable; OSTime __OSStartTime; BOOL __OSInIPL; -extern u8 __ArenaHi[]; -extern u8 __ArenaLo[]; +extern u8 __ArenaHi : 0x81800000; +extern u8 __ArenaLo : 0x803D9A60; extern u32 __DVDLongFileNameFlag; extern u32 __PADSpec; @@ -270,19 +270,19 @@ void OSInit(void) // set up bottom of heap (ArenaLo) // grab address from BootInfo if it exists, otherwise use default __ArenaLo - OSSetArenaLo((BootInfo->arenaLo == NULL) ? __ArenaLo : BootInfo->arenaLo); + OSSetArenaLo((BootInfo->arenaLo == NULL) ? &__ArenaLo : BootInfo->arenaLo); // if the input arenaLo is null, and debug flag location exists (and flag is < 2), // set arenaLo to just past the end of the db stack if ((BootInfo->arenaLo == NULL) && (BI2DebugFlag != 0) && (*BI2DebugFlag < 2)) { - debugArenaLo = (char*)(((u32)_db_stack_end + 0x1f) & ~0x1f); + debugArenaLo = (char*)(((u32)&_db_stack_end + 0x1f) & ~0x1f); OSSetArenaLo(debugArenaLo); } // set up top of heap (ArenaHi) // grab address from BootInfo if it exists, otherwise use default __ArenaHi - OSSetArenaHi((BootInfo->arenaHi == NULL) ? __ArenaHi : BootInfo->arenaHi); + OSSetArenaHi((BootInfo->arenaHi == NULL) ? &__ArenaHi : BootInfo->arenaHi); // OS INIT AND REPORT // // initialise a whole bunch of OS stuff diff --git a/src/dolphin/src/os/OSThread.c b/src/dolphin/src/os/OSThread.c index 2d2adf809..dc7991967 100644 --- a/src/dolphin/src/os/OSThread.c +++ b/src/dolphin/src/os/OSThread.c @@ -20,7 +20,7 @@ static void DefaultSwitchThreadCallback(OSThread* from, OSThread* to) { } -extern u8 _stack_addr[]; +extern u8 _stack_addr : 0x803D8A50; extern u8 _stack_end[]; #define AddTail(queue, thread, link) \ @@ -119,7 +119,7 @@ void __OSThreadInit() OSClearContext(&thread->context); OSSetCurrentContext(&thread->context); - thread->stackBase = (void*)_stack_addr; + thread->stackBase = (void*)((u32)&_stack_addr); thread->stackEnd = (void*)_stack_end; *(thread->stackEnd) = OS_THREAD_STACK_MAGIC; diff --git a/src/runtime_libs/debugger/embedded/MetroTRK/Portable/dispatch.c b/src/runtime_libs/debugger/embedded/MetroTRK/Portable/dispatch.c index cc766cb34..9faeb8b61 100644 --- a/src/runtime_libs/debugger/embedded/MetroTRK/Portable/dispatch.c +++ b/src/runtime_libs/debugger/embedded/MetroTRK/Portable/dispatch.c @@ -11,39 +11,11 @@ struct DispatchEntry gTRKDispatchTable[33] = { { &TRKDoSupportMask }, { &TRKDoCPUType }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoReadMemory }, { &TRKDoWriteMemory }, { &TRKDoReadRegisters }, { &TRKDoWriteRegisters }, - { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoFlushCache }, { &TRKDoUnsupported }, { &TRKDoContinue }, + { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoFlushCache }, { &TRKDoSetOption }, { &TRKDoContinue }, { &TRKDoStep }, { &TRKDoStop }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, { &TRKDoUnsupported }, }; -/* - * --INFO-- - * Address: 8021CEE0 - * Size: 000014 - */ -DSError TRKInitializeDispatcher() -{ - gTRKDispatchTableSize = 32; - return DS_NoError; -} - -/* - * --INFO-- - * Address: ........ - * Size: 0000A0 - */ -DSError TRKOverrideDispatch(TRKBuffer* buffer) -{ - return DS_NoError; - - // UNUSED FUNCTION -} - -/* - * --INFO-- - * Address: 8021CEF4 - * Size: 000084 - */ DSError TRKDispatchMessage(TRKBuffer* buffer) { DSError error; @@ -58,3 +30,15 @@ DSError TRKDispatchMessage(TRKBuffer* buffer) } return error; } + +DSError TRKInitializeDispatcher() +{ + gTRKDispatchTableSize = 32; + return DS_NoError; +} + +static DSError TRKOverrideDispatch(TRKBuffer* buffer) +{ + (void)buffer; + return DS_NoError; +} diff --git a/src/runtime_libs/debugger/embedded/MetroTRK/Portable/notify.c b/src/runtime_libs/debugger/embedded/MetroTRK/Portable/notify.c index cccdd005b..27a0db2cf 100644 --- a/src/runtime_libs/debugger/embedded/MetroTRK/Portable/notify.c +++ b/src/runtime_libs/debugger/embedded/MetroTRK/Portable/notify.c @@ -16,11 +16,8 @@ DSError TRKDoNotifyStopped(u8 cmd) { DSError err; int reqIdx; - TRKBuffer* msg; int bufIdx; - - // &msg - // &bufIdx + TRKBuffer* msg; err = TRKGetFreeBuffer(&bufIdx, &msg); if (err == DS_NoError) diff --git a/tools/agent_select_target.py b/tools/agent_select_target.py new file mode 100644 index 000000000..50d0af832 --- /dev/null +++ b/tools/agent_select_target.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import math +from pathlib import Path +from typing import Any + +DEFAULT_COUNT = 5 +DEFAULT_FUNCTION_COUNT = 3 +MATCH_COMPLETE = 100.0 + + +def safe_float(value: Any, default: float = 0.0) -> float: + try: + parsed = float(value) + except (TypeError, ValueError, OverflowError): + return default + return parsed if math.isfinite(parsed) else default + + +def measure_percent(measures: dict[str, Any], key: str, default: float = MATCH_COMPLETE) -> float: + return safe_float(measures.get(key, default), default) + + +def default_report_path(repo_root: Path) -> Path: + preferred = repo_root / "build" / "GQPE78" / "report.json" + if preferred.exists(): + return preferred + + candidates = sorted((repo_root / "build").glob("*/report.json")) + if candidates: + return candidates[0] + + return preferred + + +def load_report(report_path: Path) -> dict[str, Any]: + with report_path.open("r", encoding="utf-8") as report_file: + data = json.load(report_file) + return data if isinstance(data, dict) else {} + + +def is_incomplete(unit: dict[str, Any]) -> bool: + metadata = unit.get("metadata", {}) + if isinstance(metadata, dict) and metadata.get("auto_generated", False): + return False + + measures = unit.get("measures", {}) + if not isinstance(measures, dict): + return False + + return any( + safe_float(measures.get(key, MATCH_COMPLETE)) < MATCH_COMPLETE + for key in ( + "fuzzy_match_percent", + "matched_code_percent", + "matched_data_percent", + "matched_functions_percent", + ) + ) + + +def incomplete_function_rows(unit: dict[str, Any]) -> list[tuple[float, str, str]]: + rows: list[tuple[float, str, str]] = [] + functions = unit.get("functions", []) + if not isinstance(functions, list): + return rows + + for function in functions: + if not isinstance(function, dict): + continue + if "fuzzy_match_percent" not in function: + continue + + fuzzy = safe_float(function.get("fuzzy_match_percent", MATCH_COMPLETE), MATCH_COMPLETE) + if fuzzy >= MATCH_COMPLETE: + continue + + size = str(function.get("size", "?")) + name = str(function.get("name", "")) + rows.append((fuzzy, name, size)) + + rows.sort(key=lambda row: (row[0], -safe_float(row[2], 0.0), row[1])) + return rows + + +def has_real_source_path(unit: dict[str, Any]) -> bool: + metadata = unit.get("metadata", {}) + if not isinstance(metadata, dict): + return False + + source_path = metadata.get("source_path") + return isinstance(source_path, str) and bool(source_path) + + +def polish_sort_key(unit: dict[str, Any]) -> tuple[int, float, float, float, float, float, str]: + measures = unit.get("measures", {}) + if not isinstance(measures, dict): + measures = {} + + fuzzy = measure_percent(measures, "fuzzy_match_percent", 0.0) + matched_functions = measure_percent(measures, "matched_functions_percent") + matched_code = measure_percent(measures, "matched_code_percent") + matched_data = measure_percent(measures, "matched_data_percent") + name = str(unit.get("name", "")) + + # Prefer units that are nearly complete across every report axis instead of + # raw fuzzy score alone. This keeps the default list focused on clean polish + # work rather than units that still have a large code/data gap. + weakest_metric = min(fuzzy, matched_functions, matched_code, matched_data) + weighted_quality = ( + fuzzy * 0.20 + + matched_functions * 0.15 + + matched_code * 0.40 + + matched_data * 0.25 + ) + missing_metric_count = sum( + 1 + for key in ("matched_code_percent", "matched_data_percent", "matched_functions_percent") + if key not in measures + ) + source_rank = 0 if has_real_source_path(unit) else 1 + + return ( + missing_metric_count, + source_rank, + -weakest_metric, + -weighted_quality, + -matched_code, + -matched_data, + -matched_functions, + -fuzzy, + name, + ) + + +def unit_sort_key(unit: dict[str, Any], sort_mode: str) -> tuple[Any, ...]: + measures = unit.get("measures", {}) + if not isinstance(measures, dict): + measures = {} + + fuzzy = measure_percent(measures, "fuzzy_match_percent", 0.0) + matched_functions = measure_percent(measures, "matched_functions_percent") + matched_code = measure_percent(measures, "matched_code_percent") + matched_data = measure_percent(measures, "matched_data_percent") + name = str(unit.get("name", "")) + + if sort_mode == "lowest": + return (fuzzy, matched_functions, matched_code, matched_data, name) + + if sort_mode == "polish": + return polish_sort_key(unit) + + return (-fuzzy, -matched_functions, -matched_code, -matched_data, name) + + +def display_source(unit: dict[str, Any]) -> str: + metadata = unit.get("metadata", {}) + if isinstance(metadata, dict): + source_path = metadata.get("source_path") + if isinstance(source_path, str) and source_path: + return source_path + return str(unit.get("name", "")) + + +def format_measure(measures: dict[str, Any], key: str, default: float = MATCH_COMPLETE) -> str: + if key not in measures: + return " n/a" + + return f"{measure_percent(measures, key, default):6.2f}%" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="List incomplete BFBB units ranked by match score.") + parser.add_argument( + "--report", + type=Path, + help="path to report.json (defaults to build/GQPE78/report.json)", + ) + parser.add_argument( + "-n", + "--count", + type=int, + default=DEFAULT_COUNT, + help=f"number of units to print (default: {DEFAULT_COUNT})", + ) + parser.add_argument( + "--functions", + type=int, + default=DEFAULT_FUNCTION_COUNT, + help=f"number of weak functions to print per unit (default: {DEFAULT_FUNCTION_COUNT})", + ) + parser.add_argument( + "--sort", + choices=("polish", "closest", "lowest"), + default="polish", + help="rank units by balanced near-match polish, highest incomplete fuzzy match, or lowest fuzzy match", + ) + parser.add_argument( + "--list", + action="store_true", + help="print a longer list of units", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + repo_root = Path(__file__).resolve().parent.parent + report_path = (args.report or default_report_path(repo_root)).resolve() + + if not report_path.exists(): + print(f"ERROR: {report_path} not found. Run 'ninja' first.") + return 1 + + report = load_report(report_path) + units = report.get("units", []) + if not isinstance(units, list): + print(f"ERROR: {report_path} does not contain a valid units list.") + return 1 + + count = max(args.count, 1) + if args.list: + count = max(count, 15) + + incomplete_units = [unit for unit in units if isinstance(unit, dict) and is_incomplete(unit)] + if not incomplete_units: + print("No incomplete units found.") + return 0 + + incomplete_units.sort(key=lambda unit: unit_sort_key(unit, args.sort)) + if args.sort == "polish": + label = "Quality-first near-match targets" + elif args.sort == "closest": + label = "Highest-match incomplete targets" + else: + label = "Lowest-match targets" + print(f"{label} from {report_path.relative_to(repo_root).as_posix()}:") + + for index, unit in enumerate(incomplete_units[:count], 1): + measures = unit.get("measures", {}) + if not isinstance(measures, dict): + measures = {} + + fuzzy = measure_percent(measures, "fuzzy_match_percent", 0.0) + matched_code = format_measure(measures, "matched_code_percent") + matched_data = format_measure(measures, "matched_data_percent") + name = str(unit.get("name", "")) + source = display_source(unit) + print( + f"{index:2}. fuzzy {fuzzy:7.3f}% | code {matched_code} | data {matched_data} " + f"{name} {source}" + ) + + weak_functions = incomplete_function_rows(unit)[: max(args.functions, 0)] + if weak_functions: + formatted = " | ".join(f"{func_name} {func_match:.2f}% ({size}b)" for func_match, func_name, size in weak_functions) + print(f" {formatted}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())