diff --git a/include/arm11/open_agb_firm.h b/include/arm11/open_agb_firm.h new file mode 100644 index 0000000..e51ef1e --- /dev/null +++ b/include/arm11/open_agb_firm.h @@ -0,0 +1,9 @@ +#pragma once + +#include "error_codes.h" + + + +Result oafInitAndRun(void); +void oafUpdate(void); +void oafFinish(void); diff --git a/source/arm11/main.c b/source/arm11/main.c index 19f4c83..7741b1c 100644 --- a/source/arm11/main.c +++ b/source/arm11/main.c @@ -16,699 +16,41 @@ * along with this program. If not, see . */ -#include -#include -#include -#include "types.h" -#include "arm_intrinsic.h" -#include "util.h" -#include "arm11/hardware/hash.h" +#include "fs.h" +#include "hardware/gfx.h" +#include "arm11/console.h" +#include "arm11/open_agb_firm.h" #include "arm11/hardware/hid.h" #include "arm11/hardware/codec.h" -#include "hardware/lgy.h" -#include "arm11/hardware/lgyfb.h" -#include "arm11/console.h" -#include "arm11/fmt.h" -#include "arm11/hardware/mcu.h" #include "arm11/power.h" -#include "hardware/gfx.h" -#include "fs.h" -#include "fsutil.h" -#include "inih/ini.h" -#include "arm11/filebrowser.h" -#include "arm.h" -#include "arm11/hardware/lcd.h" -#include "arm11/gpu_cmd_lists.h" -#include "kernel.h" -#include "kevent.h" -#define OAF_WORK_DIR "sdmc:/3ds/open_agb_firm" -#define INI_BUF_SIZE (1024u) -#define DEFAULT_CONFIG "[general]\n" \ - "backlight=40\n" \ - "biosIntro=true\n\n" \ - "[video]\n" \ - "inGamma=2.2\n" \ - "outGamma=1.54\n" \ - "contrast=1.0\n" \ - "brightness=0.0\n" - - -typedef struct -{ - // [general] - u8 backlight; // Both LCDs. - bool biosIntro; - - // [video] - float inGamma; - float outGamma; - float contrast; - float brightness; - - // [audio] - // Maybe? - - // [input] - // TODO -} OafConfig; -static OafConfig g_oafConfig = -{ - 40, - true, - 2.2f, - 1.54f, - 1.f, - 0.f -}; - - - -static u32 padRomArea(u32 romFileSize) -{ - // Pad unused ROM area with 0xFFs (trimmed ROMs). - // Smallest retail ROM chip is 8 Mbit (1 MiB). - u32 romSize = nextPow2(romFileSize); - if(romSize < 0x100000u) romSize = 0x100000u; - memset((void*)(ROM_LOC + romFileSize), 0xFFFFFFFFu, romSize - romFileSize); - - if(romSize > 0x100000u) - { - // Fake "open bus" padding. - u32 padding = (ROM_LOC + romSize) / 2; - padding = __pkhbt(padding, padding + 1, 16); // Copy lower half + 1 to upper half. - for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += 4) - { - *(u32*)i = padding; - padding = __uadd16(padding, 0x00020002u); // Unsigned parallel halfword-wise addition. - } - } - else - { - // ROM mirroring (Classic NES Series/possibly others with 8 Mbit ROM). - // Mirror ROM across the entire 32 MiB area. - for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += romSize) - { - //memcpy((void*)i, (void*)(i - romSize), romSize); // 0x23A15DD - memcpy((void*)i, (void*)ROM_LOC, romSize); // 0x237109B - } - } - - return romSize; -} - -static Result loadGbaRom(const char *const path, u32 *const romSizeOut) -{ - Result res; - FHandle f; - if((res = fOpen(&f, path, FA_OPEN_EXISTING | FA_READ)) == RES_OK) - { - u32 fileSize; - if((fileSize = fSize(f)) <= MAX_ROM_SIZE) - { - u8 *ptr = (u8*)ROM_LOC; - u32 read; - while((res = fRead(f, ptr, 0x100000u, &read)) == RES_OK && read == 0x100000u) - ptr += 0x100000u; - - *romSizeOut = padRomArea(fileSize); - } - else res = RES_ROM_TOO_BIG; - - fClose(f); - } - - return res; -} - -typedef struct -{ - char name[200]; - char gameCode[4]; - u8 sha1[20]; - u32 attr; -} GameDbEntry; - -// Search for entry with first u64 of the SHA1 = x using binary search. -static Result searchGameDb(u64 x, GameDbEntry *const db, s32 *const entryPos) -{ - debug_printf("Database search: '%016" PRIX64 "'\n", __builtin_bswap64(x)); - - Result res; - FHandle f; - if((res = fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_READ)) == RES_OK) - { - s32 l = 0; - s32 r = fSize(f) / sizeof(GameDbEntry) - 1; // TODO: Check for 0! - while(1) - { - const s32 mid = l + (r - l) / 2; - debug_printf("l: %ld r: %ld mid: %ld\n", l, r, mid); - - if((res = fLseek(f, sizeof(GameDbEntry) * mid)) != RES_OK) break; - if((res = fRead(f, db, sizeof(GameDbEntry), NULL)) != RES_OK) break; - const u64 tmp = *(u64*)db->sha1; // Unaligned access. - if(tmp == x) - { - *entryPos = mid; // TODO: Remove. - break; - } - - if(r <= l || r < 0) - { - res = RES_NOT_FOUND; - break; - } - - if(tmp > x) r = mid - 1; - else l = mid + 1; - } - - fClose(f); - } - - return res; -} - -static u16 checkSaveOverride(u32 gameCode) -{ - if((gameCode & 0xFFu) == 'F') // Classic NES Series. - { - return SAVE_TYPE_EEPROM_8k; - } - - static const struct - { - alignas(4) char gameCode[4]; - u16 saveType; - } overrideLut[] = - { - {"\0\0\0\0", SAVE_TYPE_SRAM_256k}, // Homebrew. TODO: Set WAITCNT to 0x4014? - {"GMB\0", SAVE_TYPE_SRAM_256k}, // Goomba Color (Homebrew). - {"AA2\0", SAVE_TYPE_EEPROM_64k}, // Super Mario Advance 2. - {"A3A\0", SAVE_TYPE_EEPROM_64k}, // Super Mario Advance 3. - {"AZL\0", SAVE_TYPE_EEPROM_64k}, // Legend of Zelda, The - A Link to the Past & Four Swords. - }; - - for(u32 i = 0; i < sizeof(overrideLut) / sizeof(*overrideLut); i++) - { - // Compare Game Code without region. - if((gameCode & 0xFFFFFFu) == *((u32*)overrideLut[i].gameCode)) - { - return overrideLut[i].saveType; - } - } - - return 0xFF; -} - -static u16 tryDetectSaveType(u32 romSize) -{ - const u32 *romPtr = (u32*)ROM_LOC; - u16 saveType; - if((saveType = checkSaveOverride(romPtr[0xAC / 4])) != 0xFF) - { - debug_printf("Game Code in override list. Using save type %" PRIu16 ".\n", saveType); - return saveType; - } - - // Code based on: https://github.com/Gericom/GBARunner2/blob/master/arm9/source/save/Save.vram.cpp - romPtr += 0xE4 / 4; // Skip headers. - saveType = SAVE_TYPE_NONE; - for(; romPtr < (u32*)(ROM_LOC + romSize); romPtr++) - { - u32 tmp = *romPtr; - - // "EEPR" "FLAS" "SRAM" - if(tmp == 0x52504545u || tmp == 0x53414C46u || tmp == 0x4D415253u) - { - static const struct - { - const char *str; - u16 saveType; - } saveTypeLut[25] = - { - // EEPROM - {"EEPROM_V111", SAVE_TYPE_EEPROM_8k}, // Actually EEPROM 4k. - {"EEPROM_V120", SAVE_TYPE_EEPROM_8k}, // Confirmed. - {"EEPROM_V121", SAVE_TYPE_EEPROM_64k}, // Confirmed. - {"EEPROM_V122", SAVE_TYPE_EEPROM_8k}, // Confirmed. Except Super Mario Advance 2/3. - {"EEPROM_V124", SAVE_TYPE_EEPROM_64k}, // Confirmed. - {"EEPROM_V125", SAVE_TYPE_EEPROM_8k}, // Confirmed. - {"EEPROM_V126", SAVE_TYPE_EEPROM_8k}, // Confirmed. - - // FLASH - // Assume they all have RTC. - {"FLASH_V120", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH_V121", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH_V123", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH_V124", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH_V125", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH_V126", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH512_V130", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH512_V131", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH512_V133", SAVE_TYPE_FLASH_512k_PSC_RTC}, - {"FLASH1M_V102", SAVE_TYPE_FLASH_1m_MRX_RTC}, - {"FLASH1M_V103", SAVE_TYPE_FLASH_1m_MRX_RTC}, - - // FRAM & SRAM - {"SRAM_F_V100", SAVE_TYPE_SRAM_256k}, - {"SRAM_F_V102", SAVE_TYPE_SRAM_256k}, - {"SRAM_F_V103", SAVE_TYPE_SRAM_256k}, - - {"SRAM_V110", SAVE_TYPE_SRAM_256k}, - {"SRAM_V111", SAVE_TYPE_SRAM_256k}, - {"SRAM_V112", SAVE_TYPE_SRAM_256k}, - {"SRAM_V113", SAVE_TYPE_SRAM_256k} - }; - - for(u32 i = 0; i < 25; i++) - { - const char *const str = saveTypeLut[i].str; - u16 tmpSaveType = saveTypeLut[i].saveType; - - if(memcmp(romPtr, str, strlen(str)) == 0) - { - if(tmpSaveType == SAVE_TYPE_EEPROM_8k || tmpSaveType == SAVE_TYPE_EEPROM_64k) - { - // If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2. - if(romSize > 0x1000000) tmpSaveType++; - } - saveType = tmpSaveType; - debug_printf("Detected SDK save type '%s'.\n", str); - goto saveTypeFound; - } - } - } - } - -saveTypeFound: - - return saveType; -} - -static u16 saveDbDebug(const char *const savePath, u32 romSize) -{ - FILINFO fi; - const bool saveExists = fStat(savePath, &fi) == RES_OK; - const u16 autoSaveType = tryDetectSaveType(romSize); - - // TODO: Check for homebrew before searching the db. - u64 sha1[3]; - hash((u32*)ROM_LOC, romSize, (u32*)sha1, HASH_INPUT_BIG | HASH_MODE_1, HASH_OUTPUT_BIG); - - Result res; - GameDbEntry dbEntry; - s32 dbPos = -1; - u16 saveType = SAVE_TYPE_NONE; - if((res = searchGameDb(*sha1, &dbEntry, &dbPos)) == RES_OK) saveType = dbEntry.attr & 0xFu; - else - { - ee_puts("Could not access the game db! Press the power button twice."); - printErrorWaitInput(res, 0); - return SAVE_TYPE_NONE; - } - - consoleClear(); - ee_printf("Save file (Press (X) to delete): %s\n" - "Save type (from db): %u\n" - "Save type (auto detect): %u\n\n" - " EEPROM 4k/8k (0, 1)\n" - " EEPROM 64k (2, 3)\n" - " Flash 512k RTC (4, 6, 8)\n" - " Flash 512k (5, 7, 9)\n" - " Flash 1m RTC (10, 12)\n" - " Flash 1m (11, 13)\n" - " SRAM 256k (14)\n" - " None (15)\n\n\n", (saveExists ? "found" : "not found"), saveType, autoSaveType); - ee_puts("Please note:\n" - "- Auto detection is broken for EEPROM save types.\n" - "- Choose the lowest size save type first and work your way up until the game fully works.\n" - "- If the game works with a Flash save type try without RTC first.\n" - "- Delete the save before you try a new save type.\n" - "- Make sure all your dumps are verified good dumps (no-intro.org)!"); - - static const u8 saveTypeCursorLut[16] = {0, 0, 1, 1, 2, 3, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7}; - u8 oldCursor = 0; - u8 cursor = saveTypeCursorLut[saveType]; - while(1) - { - ee_printf("\x1b[%u;H ", oldCursor + 4); - ee_printf("\x1b[%u;H>", cursor + 4); - oldCursor = cursor; - - u32 kDown; - do - { - GFX_waitForVBlank0(); - - hidScanInput(); - if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) goto end; - kDown = hidKeysDown(); - } while(kDown == 0); - - if((kDown & KEY_DUP) && cursor > 0) cursor--; - else if((kDown & KEY_DDOWN) && cursor < 7) cursor++; - else if(kDown & KEY_X) - { - fUnlink(savePath); - ee_printf("\x1b[0;33Hdeleted "); - } - else if(kDown & KEY_A) break; - } - - static const u8 cursorSaveTypeLut[8] = {0, 2, 8, 9, 10, 11, 14, 15}; - saveType = cursorSaveTypeLut[cursor]; - if(saveType == SAVE_TYPE_EEPROM_8k || saveType == SAVE_TYPE_EEPROM_64k) - { - // If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2. - if(romSize > 0x1000000) saveType++; - } - if(dbEntry.attr != saveType) - { - if(dbPos > -1 || dbPos < 3256) - { - dbEntry.attr = saveType; - FHandle f; - if(fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_WRITE) == RES_OK) - { - fLseek(f, (sizeof(GameDbEntry) * dbPos) + offsetof(GameDbEntry, attr)); - fWrite(f, &dbEntry.attr, sizeof(dbEntry.attr), NULL); - fClose(f); - } - else - { - ee_puts("Could not open db for write!"); - saveType = SAVE_TYPE_NONE; - } - } - else - { - ee_puts("Db position out of range!"); - saveType = SAVE_TYPE_NONE; - } - } - -end: - return saveType; -} - -static void adjustGammaTableForGba(void) -{ - const float inGamma = g_oafConfig.inGamma; - const float outGamma = g_oafConfig.outGamma; - const float contrast = g_oafConfig.contrast; - const float brightness = g_oafConfig.brightness; - for(u32 i = 0; i < 256; i++) - { - // Credits for this algo go to Extrems. - // Originally from Game Boy Interface Standard Edition for the GameCube. - u32 res = powf(powf(contrast, inGamma) * powf((float)i / 255.0f + brightness / contrast, inGamma), - 1.0f / outGamma) * 255.0f; - if(res > 255) res = 255; - - // Same adjustment for red/green/blue. - REG_LCD_PDC0_GTBL_FIFO = res<<16 | res<<8 | res; - } -} - -static Result dumpFrameTex(void) -{ - // 512x-512 (hight negative to flip vertically). - // Pixels at offset 0x40. - alignas(4) static const u8 bmpHeader[54] = - { - 0x42, 0x4D, 0x40, 0x00, 0x0C, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x28, 0x00, - 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFE, - 0xFF, 0xFF, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x13, 0x0B, - 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - /*GX_displayTransfer((u32*)0x18200000, 160u<<16 | 256u, (u32*)0x18400000, 160u<<16 | 256u, 1u<<12 | 1u<<8); - GFX_waitForPPF(); - //fsQuickWrite("sdmc:/lgyfb_dbg_frame.bgr", (void*)0x18400000, 256 * 160 * 3);*/ - GX_displayTransfer((u32*)0x18200000, 240u<<16 | 512u, (u32*)0x18400040, 240u<<16 | 512u, 1u<<12 | 1u<<8); - GFX_waitForPPF(); - - memcpy((void*)0x18400000, bmpHeader, sizeof(bmpHeader)); - - return fsQuickWrite("texture_dump.bmp", (void*)0x18400000, 0x40 + 512 * 512 * 3); -} - -static void gbaGfxHandler(void *args) -{ - KEvent *const event = (KEvent*)args; - - while(1) - { - if(waitForEvent(event) != KRES_OK) break; - clearEvent(event); - - // Rotate the frame using the GPU. - // 240x160: TODO. - // 360x240: about 0.623620315 ms. - static bool inited = false; - u32 listSize; - const u32 *list; - if(inited == false) - { - inited = true; - - listSize = sizeof(gbaGpuInitList); - list = (u32*)gbaGpuInitList; - } - else - { - listSize = sizeof(gbaGpuList2); - list = (u32*)gbaGpuList2; - } - GX_processCommandList(listSize, list); - GFX_waitForP3D(); - GX_displayTransfer((u32*)(0x18180000 + (16 * 240 * 3)), 368u<<16 | 240u, - GFX_getFramebuffer(SCREEN_TOP) + (16 * 240 * 3), 368u<<16 | 240u, 1u<<12 | 1u<<8); - GFX_waitForPPF(); - GFX_swapFramebufs(); - - if(hidKeysDown() & KEY_Y) dumpFrameTex(); - } - - taskExit(); -} - -#ifndef NDEBUG -static void debugTests(void) -{ - const u32 kDown = hidKeysDown(); - - // Print GBA RTC date/time. - if(kDown & KEY_X) - { - GbaRtc rtc; LGY_getGbaRtc(&rtc); - ee_printf("RTC: %02X.%02X.%04X %02X:%02X:%02X\n", rtc.d, rtc.mon, rtc.y + 0x2000u, rtc.h, rtc.min, rtc.s); - - // Trigger Game Boy Player enhancements. - // Needs to be done on the Game Boy Player logo screen. - // 2 frames nothing pressed and 1 frame all D-Pad buttons pressed. - /*REG_LGY_PAD_SEL = 0x1FFF; // Override all buttons. - static u8 gbp = 2; - if(gbp > 0) - { - REG_LGY_PAD_VAL = 0x1FFF; // Force all buttons not pressed. - gbp--; - } - else - { - REG_LGY_PAD_VAL = 0x1F0F; // All D-Pad buttons pressed. - gbp = 2; - }*/ - } - //else REG_LGY_PAD_SEL = 0; // Stop overriding buttons. -} -#endif - -static int confIniCallback(void* user, const char* section, const char* name, const char* value) -{ - OafConfig *const config = (OafConfig*)user; - - if(strcmp(section, "general") == 0) - { - if(strcmp(name, "backlight") == 0) - config->backlight = (u8)strtoul(value, NULL, 10); - else if(strcmp(name, "biosIntro") == 0) - config->biosIntro = (strcmp(value, "true") == 0 ? true : false); - } - else if(strcmp(section, "video") == 0) - { - if(strcmp(name, "inGamma") == 0) - config->inGamma = str2float(value); - else if(strcmp(name, "outGamma") == 0) - config->outGamma = str2float(value); - else if(strcmp(name, "contrast") == 0) - config->contrast = str2float(value); - else if(strcmp(name, "brightness") == 0) - config->brightness = str2float(value); - } - /*else if(strcmp(section, "audio") == 0) - { - } - else if(strcmp(section, "input") == 0) - { - }*/ - else return 0; // Error. - - return 1; // 1 is no error? Really? -} - -static Result parseMainConfig(void) -{ - char *iniBuf = (char*)calloc(INI_BUF_SIZE, 1); - if(iniBuf == NULL) return RES_OUT_OF_MEM; - - Result res = fsQuickRead("config.ini", iniBuf, INI_BUF_SIZE - 1); - if(res == RES_OK) ini_parse_string(iniBuf, confIniCallback, &g_oafConfig); - else - { - const char *const defaultConfig = DEFAULT_CONFIG; - res = fsQuickWrite("config.ini", defaultConfig, strlen(defaultConfig)); - } - - // Apply backlight brightness. - // TODO: Move this elsewhere. - // FIXME: If any error happens before this point it will be invisible. - const u8 backlight = g_oafConfig.backlight; - GFX_setBrightness(backlight, backlight); - - free(iniBuf); - - return res; -} - -static Result handleFsStuff(char romPath[512]) -{ - // Mount SD card. - Result res = fMount(FS_DRIVE_SDMC); - if(res == RES_OK) - { - char *lastDir = (char*)calloc(512, 1); - if(lastDir != NULL) - { - do - { - // Create the work dir and switch to it. - if((res = fsMakePath(OAF_WORK_DIR)) != RES_OK && res != RES_FR_EXIST) break; - if((res = fChdir(OAF_WORK_DIR)) != RES_OK) break; - - // Parse config. - parseMainConfig(); - - // Get last ROM launch path. - if((res = fsQuickRead("lastdir.bin", lastDir, 511)) != RES_OK) - { - if(res == RES_FR_NO_FILE) strcpy(lastDir, "sdmc:/"); - else break; - } - - // Show file browser. - *romPath = '\0'; - if((res = browseFiles(lastDir, romPath)) != RES_OK) - { - // Second chance in case the last dir has been deleted. - if(res == RES_FR_NO_PATH) - { - strcpy(lastDir, "sdmc:/"); - if((res = browseFiles(lastDir, romPath)) != RES_OK) break; - } - else break; - } - - size_t cmpLen = strrchr(romPath, '/') - romPath; - if((size_t)(strchr(romPath, '/') - romPath) == cmpLen) cmpLen++; // Keep the first '/'. - if(cmpLen < 512) - { - if(cmpLen < strlen(lastDir) || strncmp(lastDir, romPath, cmpLen) != 0) - { - strncpy(lastDir, romPath, cmpLen); - lastDir[cmpLen] = '\0'; - res = fsQuickWrite("lastdir.bin", lastDir, cmpLen + 1); - } - } - } while(0); - - free(lastDir); - } - else res = RES_OUT_OF_MEM; - } - - return res; -} int main(void) { + Result res = fMount(FS_DRIVE_SDMC); GFX_init(GFX_BGR8, GFX_RGB565); consoleInit(SCREEN_BOT, NULL); //CODEC_init(); - char *romPath = (char*)malloc(512); - Result res = handleFsStuff(romPath); - if(res != RES_OK || *romPath == '\0') goto end; - - ee_puts("Reading ROM and save..."); - u32 romSize; - res = loadGbaRom(romPath, &romSize); - if(res != RES_OK) goto end; - - // Detect save type and adjust path for the save file. - const u16 saveType = tryDetectSaveType(romSize); - const u32 romPathLen = strlen(romPath); - strcpy(romPath + romPathLen - 4, ".sav"); - /*const u32 romPathLen = strlen(romPath); - strcpy(romPath + romPathLen - 4, ".sav"); - const u16 saveType = saveDbDebug(romPath, romSize);*/ - - // Prepare ARM9 for GBA mode + settings and save loading. - if((res = LGY_prepareGbaMode(g_oafConfig.biosIntro, saveType, romPath)) == RES_OK) + if(res == RES_OK && (res = oafInitAndRun()) == RES_OK) { -#ifdef NDEBUG - GFX_setForceBlack(false, true); - // Don't turn the backlight off on 2DS. - if(MCU_getSystemModel() != 3) GFX_powerOffBacklights(GFX_BLIGHT_BOT); -#endif - - KEvent *const frameReadyEvent = createEvent(false); - LGYFB_init(frameReadyEvent); // Setup Legacy Framebuffer. - /*KTask *const gfxTask =*/ createTask(0x800, 3, gbaGfxHandler, frameReadyEvent); - - // Adjust gamma table and sync LgyFb start with LCD VBlank. - adjustGammaTableForGba(); - GFX_waitForVBlank0(); - LGY_switchMode(); - do { hidScanInput(); if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) break; - LGY_handleOverrides(); - - waitForEvent(frameReadyEvent); + oafUpdate(); } while(1); - LGYFB_deinit(); - deleteEvent(frameReadyEvent); // gbaGfxHandler() will automatically terminate. + oafFinish(); } + else printErrorWaitInput(res, 0); -end: - free(romPath); - if(res != RES_OK) printErrorWaitInput(res, 0); - - LGY_deinit(); - fUnmount(FS_DRIVE_SDMC); CODEC_deinit(); GFX_deinit(); + fUnmount(FS_DRIVE_SDMC); + power_off(); return 0; diff --git a/source/arm11/open_agb_firm.c b/source/arm11/open_agb_firm.c new file mode 100644 index 0000000..d3f41e7 --- /dev/null +++ b/source/arm11/open_agb_firm.c @@ -0,0 +1,709 @@ +/* + * This file is part of fastboot 3DS + * Copyright (C) 2017 derrek, profi200 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include "types.h" +#include "arm_intrinsic.h" +#include "util.h" +#include "arm11/hardware/hash.h" +#include "arm11/hardware/hid.h" +#include "hardware/lgy.h" +#include "arm11/hardware/lgyfb.h" +#include "arm11/console.h" +#include "arm11/fmt.h" +#include "hardware/gfx.h" +#include "fs.h" +#include "fsutil.h" +#include "inih/ini.h" +#include "arm11/filebrowser.h" +#include "arm11/hardware/lcd.h" +#include "arm11/gpu_cmd_lists.h" +#include "arm11/hardware/mcu.h" +#include "kernel.h" +#include "kevent.h" + + +//#define OAF_SAVE_DB_DEBUG (1) +#define OAF_WORK_DIR "sdmc:/3ds/open_agb_firm" +#define INI_BUF_SIZE (1024u) +#define DEFAULT_CONFIG "[general]\n" \ + "backlight=40\n" \ + "biosIntro=true\n\n" \ + "[video]\n" \ + "inGamma=2.2\n" \ + "outGamma=1.54\n" \ + "contrast=1.0\n" \ + "brightness=0.0\n" + + +typedef struct +{ + // [general] + u8 backlight; // Both LCDs. + bool biosIntro; + + // [video] + float inGamma; + float outGamma; + float contrast; + float brightness; +} OafConfig; + +typedef struct +{ + // [game] + u16 saveType; + u8 saveSlot; +} OafGameConfig; + +typedef struct +{ + char name[200]; + char gameCode[4]; + u8 sha1[20]; + u32 attr; +} GameDbEntry; + + +// Default config. +static OafConfig g_oafConfig = +{ + 40, + true, + 2.2f, + 1.54f, + 1.f, + 0.f +}; +static KEvent *g_frameReadyEvent = NULL; + + + +static u32 fixRomPadding(u32 romFileSize) +{ + // Pad unused ROM area with 0xFFs (trimmed ROMs). + // Smallest retail ROM chip is 8 Mbit (1 MiB). + u32 romSize = nextPow2(romFileSize); + if(romSize < 0x100000u) romSize = 0x100000u; + memset((void*)(ROM_LOC + romFileSize), 0xFFFFFFFFu, romSize - romFileSize); + + if(romSize > 0x100000u) + { + // Fake "open bus" padding. + u32 padding = (ROM_LOC + romSize) / 2; + padding = __pkhbt(padding, padding + 1, 16); // Copy lower half + 1 to upper half. + for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += 4) + { + *(u32*)i = padding; + padding = __uadd16(padding, 0x00020002u); // Unsigned parallel halfword-wise addition. + } + } + else + { + // ROM mirroring (Classic NES Series/possibly others with 8 Mbit ROM). + // Mirror ROM across the entire 32 MiB area. + for(uintptr_t i = ROM_LOC + romSize; i < ROM_LOC + MAX_ROM_SIZE; i += romSize) + { + //memcpy((void*)i, (void*)(i - romSize), romSize); // 0x23A15DD + memcpy((void*)i, (void*)ROM_LOC, romSize); // 0x237109B + } + } + + return romSize; +} + +static Result loadGbaRom(const char *const path, u32 *const romSizeOut) +{ + Result res; + FHandle f; + if((res = fOpen(&f, path, FA_OPEN_EXISTING | FA_READ)) == RES_OK) + { + u32 fileSize; + if((fileSize = fSize(f)) <= MAX_ROM_SIZE) + { + u8 *ptr = (u8*)ROM_LOC; + u32 read; + while((res = fRead(f, ptr, 0x100000u, &read)) == RES_OK && read == 0x100000u) + ptr += 0x100000u; + + *romSizeOut = fixRomPadding(fileSize); + } + else res = RES_ROM_TOO_BIG; + + fClose(f); + } + + return res; +} + +// Search for entry with first u64 of the SHA1 = x using binary search. +static Result searchGameDb(u64 x, GameDbEntry *const db, s32 *const entryPos) +{ + debug_printf("Database search: '%016" PRIX64 "'\n", __builtin_bswap64(x)); + + Result res; + FHandle f; + if((res = fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_READ)) == RES_OK) + { + s32 l = 0; + s32 r = fSize(f) / sizeof(GameDbEntry) - 1; // TODO: Check for 0! + while(1) + { + const s32 mid = l + (r - l) / 2; + debug_printf("l: %ld r: %ld mid: %ld\n", l, r, mid); + + if((res = fLseek(f, sizeof(GameDbEntry) * mid)) != RES_OK) break; + if((res = fRead(f, db, sizeof(GameDbEntry), NULL)) != RES_OK) break; + const u64 tmp = *(u64*)db->sha1; // Unaligned access. + if(tmp == x) + { + *entryPos = mid; // TODO: Remove. + break; + } + + if(r <= l || r < 0) + { + res = RES_NOT_FOUND; + break; + } + + if(tmp > x) r = mid - 1; + else l = mid + 1; + } + + fClose(f); + } + + return res; +} + +static u16 checkSaveOverride(u32 gameCode) +{ + if((gameCode & 0xFFu) == 'F') // Classic NES Series. + { + return SAVE_TYPE_EEPROM_8k; + } + + static const struct + { + alignas(4) char gameCode[4]; + u16 saveType; + } overrideLut[] = + { + {"\0\0\0\0", SAVE_TYPE_SRAM_256k}, // Homebrew. TODO: Set WAITCNT to 0x4014? + {"GMB\0", SAVE_TYPE_SRAM_256k}, // Goomba Color (Homebrew). + {"AA2\0", SAVE_TYPE_EEPROM_64k}, // Super Mario Advance 2. + {"A3A\0", SAVE_TYPE_EEPROM_64k}, // Super Mario Advance 3. + {"AZL\0", SAVE_TYPE_EEPROM_64k}, // Legend of Zelda, The - A Link to the Past & Four Swords. + }; + + for(u32 i = 0; i < sizeof(overrideLut) / sizeof(*overrideLut); i++) + { + // Compare Game Code without region. + if((gameCode & 0xFFFFFFu) == *((u32*)overrideLut[i].gameCode)) + { + return overrideLut[i].saveType; + } + } + + return 0xFF; +} + +static u16 tryDetectSaveType(u32 romSize) +{ + const u32 *romPtr = (u32*)ROM_LOC; + u16 saveType; + if((saveType = checkSaveOverride(romPtr[0xAC / 4])) != 0xFF) + { + debug_printf("Game Code in override list. Using save type %" PRIu16 ".\n", saveType); + return saveType; + } + + // Code based on: https://github.com/Gericom/GBARunner2/blob/master/arm9/source/save/Save.vram.cpp + romPtr += 0xE4 / 4; // Skip headers. + saveType = SAVE_TYPE_NONE; + for(; romPtr < (u32*)(ROM_LOC + romSize); romPtr++) + { + u32 tmp = *romPtr; + + // "EEPR" "FLAS" "SRAM" + if(tmp == 0x52504545u || tmp == 0x53414C46u || tmp == 0x4D415253u) + { + static const struct + { + const char *str; + u16 saveType; + } saveTypeLut[25] = + { + // EEPROM + {"EEPROM_V111", SAVE_TYPE_EEPROM_8k}, // Actually EEPROM 4k. + {"EEPROM_V120", SAVE_TYPE_EEPROM_8k}, // Confirmed. + {"EEPROM_V121", SAVE_TYPE_EEPROM_64k}, // Confirmed. + {"EEPROM_V122", SAVE_TYPE_EEPROM_8k}, // Confirmed. Except Super Mario Advance 2/3. + {"EEPROM_V124", SAVE_TYPE_EEPROM_64k}, // Confirmed. + {"EEPROM_V125", SAVE_TYPE_EEPROM_8k}, // Confirmed. + {"EEPROM_V126", SAVE_TYPE_EEPROM_8k}, // Confirmed. + + // FLASH + // Assume they all have RTC. + {"FLASH_V120", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH_V121", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH_V123", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH_V124", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH_V125", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH_V126", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH512_V130", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH512_V131", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH512_V133", SAVE_TYPE_FLASH_512k_PSC_RTC}, + {"FLASH1M_V102", SAVE_TYPE_FLASH_1m_MRX_RTC}, + {"FLASH1M_V103", SAVE_TYPE_FLASH_1m_MRX_RTC}, + + // FRAM & SRAM + {"SRAM_F_V100", SAVE_TYPE_SRAM_256k}, + {"SRAM_F_V102", SAVE_TYPE_SRAM_256k}, + {"SRAM_F_V103", SAVE_TYPE_SRAM_256k}, + + {"SRAM_V110", SAVE_TYPE_SRAM_256k}, + {"SRAM_V111", SAVE_TYPE_SRAM_256k}, + {"SRAM_V112", SAVE_TYPE_SRAM_256k}, + {"SRAM_V113", SAVE_TYPE_SRAM_256k} + }; + + for(u32 i = 0; i < 25; i++) + { + const char *const str = saveTypeLut[i].str; + u16 tmpSaveType = saveTypeLut[i].saveType; + + if(memcmp(romPtr, str, strlen(str)) == 0) + { + if(tmpSaveType == SAVE_TYPE_EEPROM_8k || tmpSaveType == SAVE_TYPE_EEPROM_64k) + { + // If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2. + if(romSize > 0x1000000) tmpSaveType++; + } + saveType = tmpSaveType; + debug_printf("Detected SDK save type '%s'.\n", str); + goto saveTypeFound; + } + } + } + } + +saveTypeFound: + + return saveType; +} + +static u16 saveDbDebug(const char *const savePath, u32 romSize) +{ + FILINFO fi; + const bool saveExists = fStat(savePath, &fi) == RES_OK; + const u16 autoSaveType = tryDetectSaveType(romSize); + + // TODO: Check for homebrew before searching the db. + u64 sha1[3]; + hash((u32*)ROM_LOC, romSize, (u32*)sha1, HASH_INPUT_BIG | HASH_MODE_1, HASH_OUTPUT_BIG); + + Result res; + GameDbEntry dbEntry; + s32 dbPos = -1; + u16 saveType = SAVE_TYPE_NONE; + if((res = searchGameDb(*sha1, &dbEntry, &dbPos)) == RES_OK) saveType = dbEntry.attr & 0xFu; + else + { + ee_puts("Could not access the game db! Press the power button twice."); + printErrorWaitInput(res, 0); + return SAVE_TYPE_NONE; + } + + consoleClear(); + ee_printf("Save file (Press (X) to delete): %s\n" + "Save type (from db): %u\n" + "Save type (auto detect): %u\n\n" + " EEPROM 4k/8k (0, 1)\n" + " EEPROM 64k (2, 3)\n" + " Flash 512k RTC (4, 6, 8)\n" + " Flash 512k (5, 7, 9)\n" + " Flash 1m RTC (10, 12)\n" + " Flash 1m (11, 13)\n" + " SRAM 256k (14)\n" + " None (15)\n\n\n", (saveExists ? "found" : "not found"), saveType, autoSaveType); + ee_puts("Please note:\n" + "- Auto detection is broken for EEPROM save types.\n" + "- Choose the lowest size save type first and work your way up until the game fully works.\n" + "- If the game works with a Flash save type try without RTC first.\n" + "- Delete the save before you try a new save type.\n" + "- Make sure all your dumps are verified good dumps (no-intro.org)!"); + + static const u8 saveTypeCursorLut[16] = {0, 0, 1, 1, 2, 3, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7}; + u8 oldCursor = 0; + u8 cursor = saveTypeCursorLut[saveType]; + while(1) + { + ee_printf("\x1b[%u;H ", oldCursor + 4); + ee_printf("\x1b[%u;H>", cursor + 4); + oldCursor = cursor; + + u32 kDown; + do + { + GFX_waitForVBlank0(); + + hidScanInput(); + if(hidGetExtraKeys(0) & (KEY_POWER_HELD | KEY_POWER)) goto end; + kDown = hidKeysDown(); + } while(kDown == 0); + + if((kDown & KEY_DUP) && cursor > 0) cursor--; + else if((kDown & KEY_DDOWN) && cursor < 7) cursor++; + else if(kDown & KEY_X) + { + fUnlink(savePath); + ee_printf("\x1b[0;33Hdeleted "); + } + else if(kDown & KEY_A) break; + } + + static const u8 cursorSaveTypeLut[8] = {0, 2, 8, 9, 10, 11, 14, 15}; + saveType = cursorSaveTypeLut[cursor]; + if(saveType == SAVE_TYPE_EEPROM_8k || saveType == SAVE_TYPE_EEPROM_64k) + { + // If ROM bigger than 16 MiB --> SAVE_TYPE_EEPROM_8k_2 or SAVE_TYPE_EEPROM_64k_2. + if(romSize > 0x1000000) saveType++; + } + if(dbEntry.attr != saveType) + { + if(dbPos > -1 || dbPos < 3253) + { + dbEntry.attr = (intLog2(romSize)<<27) | saveType; + FHandle f; + if(fOpen(&f, "gba_db.bin", FA_OPEN_EXISTING | FA_WRITE) == RES_OK) + { + fLseek(f, (sizeof(GameDbEntry) * dbPos) + offsetof(GameDbEntry, attr)); + fWrite(f, &dbEntry.attr, sizeof(dbEntry.attr), NULL); + fClose(f); + } + else + { + ee_puts("Could not open db for write!"); + saveType = SAVE_TYPE_NONE; + } + } + else + { + ee_puts("Db position out of range!"); + saveType = SAVE_TYPE_NONE; + } + } + +end: + return saveType; +} + +static void adjustGammaTableForGba(void) +{ + const float inGamma = g_oafConfig.inGamma; + const float outGamma = g_oafConfig.outGamma; + const float contrast = g_oafConfig.contrast; + const float brightness = g_oafConfig.brightness; + for(u32 i = 0; i < 256; i++) + { + // Credits for this algo go to Extrems. + // Originally from Game Boy Interface Standard Edition for the GameCube. + u32 res = powf(powf(contrast, inGamma) * powf((float)i / 255.0f + brightness / contrast, inGamma), + 1.0f / outGamma) * 255.0f; + if(res > 255) res = 255; + + // Same adjustment for red/green/blue. + REG_LCD_PDC0_GTBL_FIFO = res<<16 | res<<8 | res; + } +} + +static Result dumpFrameTex(void) +{ + // 512x-512 (hight negative to flip vertically). + // Pixels at offset 0x40. + alignas(4) static const u8 bmpHeader[54] = + { + 0x42, 0x4D, 0x40, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x28, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFE, + 0xFF, 0xFF, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x13, 0x0B, + 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + /*GX_displayTransfer((u32*)0x18200000, 160u<<16 | 256u, (u32*)0x18400000, 160u<<16 | 256u, 1u<<12 | 1u<<8); + GFX_waitForPPF(); + //fsQuickWrite("sdmc:/lgyfb_dbg_frame.bgr", (void*)0x18400000, 256 * 160 * 3);*/ + GX_displayTransfer((u32*)0x18200000, 240u<<16 | 512u, (u32*)0x18400040, 240u<<16 | 512u, 1u<<12 | 1u<<8); + GFX_waitForPPF(); + + memcpy((void*)0x18400000, bmpHeader, sizeof(bmpHeader)); + + return fsQuickWrite("texture_dump.bmp", (void*)0x18400000, 0x40 + 512 * 512 * 3); +} + +static void gbaGfxHandler(void *args) +{ + KEvent *const event = (KEvent*)args; + + while(1) + { + if(waitForEvent(event) != KRES_OK) break; + clearEvent(event); + + // Rotate the frame using the GPU. + // 240x160: TODO. + // 360x240: about 0.623620315 ms. + static bool inited = false; + u32 listSize; + const u32 *list; + if(inited == false) + { + inited = true; + + listSize = sizeof(gbaGpuInitList); + list = (u32*)gbaGpuInitList; + } + else + { + listSize = sizeof(gbaGpuList2); + list = (u32*)gbaGpuList2; + } + GX_processCommandList(listSize, list); + GFX_waitForP3D(); + GX_displayTransfer((u32*)(0x18180000 + (16 * 240 * 3)), 368u<<16 | 240u, + GFX_getFramebuffer(SCREEN_TOP) + (16 * 240 * 3), 368u<<16 | 240u, 1u<<12 | 1u<<8); + GFX_waitForPPF(); + GFX_swapFramebufs(); + + if(hidKeysDown() & KEY_Y) dumpFrameTex(); + } + + taskExit(); +} + +static int confIniHandler(void* user, const char* section, const char* name, const char* value) +{ + OafConfig *const config = (OafConfig*)user; + + if(strcmp(section, "general") == 0) + { + if(strcmp(name, "backlight") == 0) + config->backlight = (u8)strtoul(value, NULL, 10); + else if(strcmp(name, "biosIntro") == 0) + config->biosIntro = (strcmp(value, "true") == 0 ? true : false); + } + else if(strcmp(section, "video") == 0) + { + if(strcmp(name, "inGamma") == 0) + config->inGamma = str2float(value); + else if(strcmp(name, "outGamma") == 0) + config->outGamma = str2float(value); + else if(strcmp(name, "contrast") == 0) + config->contrast = str2float(value); + else if(strcmp(name, "brightness") == 0) + config->brightness = str2float(value); + } + /*else if(strcmp(section, "audio") == 0) + { + } + else if(strcmp(section, "input") == 0) + { + }*/ + else return 0; // Error. + + return 1; // 1 is no error? Really? +} + +static int gameConfIniHandler(void* user, const char* section, const char* name, const char* value) +{ + //OafGameConfig *const config = (OafGameConfig*)user; + + /*if(strcmp(section, "game") == 0) + { + // Save type. + // Save slot. + } + else if(strcmp(section, "video") == 0) + { + if(strcmp(name, "inGamma") == 0) + config->inGamma = str2float(value); + else if(strcmp(name, "outGamma") == 0) + config->outGamma = str2float(value); + else if(strcmp(name, "contrast") == 0) + config->contrast = str2float(value); + else if(strcmp(name, "brightness") == 0) + config->brightness = str2float(value); + } + else if(strcmp(section, "audio") == 0) + { + } + else if(strcmp(section, "input") == 0) + { + } + else return 0;*/ // Error. + + return 1; // 1 is no error? Really? +} + +static Result parseConfig(const char *const path, u8 confType, void *config) +{ + char *iniBuf = (char*)calloc(INI_BUF_SIZE, 1); + if(iniBuf == NULL) return RES_OUT_OF_MEM; + + Result res = fsQuickRead(path, iniBuf, INI_BUF_SIZE - 1); + if(res == RES_OK) ini_parse_string(iniBuf, (confType == 0 ? confIniHandler : gameConfIniHandler), config); + else + { + const char *const defaultConfig = DEFAULT_CONFIG; + res = fsQuickWrite(path, defaultConfig, strlen(defaultConfig)); + } + + free(iniBuf); + + return res; +} + +static Result handleFsStuff(char romAndSavePath[512]) +{ + Result res; + char *lastDir = (char*)calloc(512, 1); + if(lastDir != NULL) + { + do + { + // Create the work dir and switch to it. + if((res = fsMakePath(OAF_WORK_DIR)) != RES_OK && res != RES_FR_EXIST) break; + if((res = fChdir(OAF_WORK_DIR)) != RES_OK) break; + + // Parse config. + parseConfig("config.ini", 0, &g_oafConfig); + { // TODO: Move this elsewhere? + const u8 backlight = g_oafConfig.backlight; + GFX_setBrightness(backlight, backlight); + } + + // Get last ROM launch path. + if((res = fsQuickRead("lastdir.bin", lastDir, 511)) != RES_OK) + { + if(res == RES_FR_NO_FILE) strcpy(lastDir, "sdmc:/"); + else break; + } + + // Show file browser. + *romAndSavePath = '\0'; + if((res = browseFiles(lastDir, romAndSavePath)) == RES_FR_NO_PATH) + { + // Second chance in case the last dir has been deleted. + strcpy(lastDir, "sdmc:/"); + if((res = browseFiles(lastDir, romAndSavePath)) != RES_OK) break; + } + else if(res != RES_OK) break; + + size_t cmpLen = strrchr(romAndSavePath, '/') - romAndSavePath; + if((size_t)(strchr(romAndSavePath, '/') - romAndSavePath) == cmpLen) cmpLen++; // Keep the first '/'. + if(cmpLen < 512) + { + if(cmpLen < strlen(lastDir) || strncmp(lastDir, romAndSavePath, cmpLen) != 0) + { + strncpy(lastDir, romAndSavePath, cmpLen); + lastDir[cmpLen] = '\0'; + res = fsQuickWrite("lastdir.bin", lastDir, cmpLen + 1); + } + } + } while(0); + + free(lastDir); + } + else res = RES_OUT_OF_MEM; + + return res; +} + +Result oafInitAndRun(void) +{ + Result res; + char *const romAndSavePath = (char*)malloc(512); + if(romAndSavePath != NULL) + { + do + { + if((res = handleFsStuff(romAndSavePath)) != RES_OK || *romAndSavePath == '\0') break; + + ee_puts("Loading..."); + u32 romSize; + if((res = loadGbaRom(romAndSavePath, &romSize)) != RES_OK) break; + +#ifndef OAF_SAVE_DB_DEBUG + // Detect save type and adjust path for the save file. + const u16 saveType = tryDetectSaveType(romSize); + strcpy(romAndSavePath + strlen(romAndSavePath) - 4, ".sav"); +#else + strcpy(romAndSavePath + strlen(romAndSavePath) - 4, ".sav"); + const u16 saveType = saveDbDebug(romAndSavePath, romSize); +#endif + + // Prepare ARM9 for GBA mode + settings and save loading. + if((res = LGY_prepareGbaMode(g_oafConfig.biosIntro, saveType, romAndSavePath)) == RES_OK) + { +#ifdef NDEBUG + GFX_setForceBlack(false, true); + // Don't turn the backlight off on 2DS. + if(MCU_getSystemModel() != 3) GFX_powerOffBacklights(GFX_BLIGHT_BOT); +#endif + + KEvent *const frameReadyEvent = createEvent(false); + LGYFB_init(frameReadyEvent); // Setup Legacy Framebuffer. + createTask(0x800, 3, gbaGfxHandler, frameReadyEvent); + g_frameReadyEvent = frameReadyEvent; + + // Adjust gamma table and sync LgyFb start with LCD VBlank. + adjustGammaTableForGba(); + GFX_waitForVBlank0(); + LGY_switchMode(); + } + } while(0); + } + else res = RES_OUT_OF_MEM; + + free(romAndSavePath); + + return res; +} + +void oafUpdate(void) +{ + LGY_handleOverrides(); + waitForEvent(g_frameReadyEvent); +} + +void oafFinish(void) +{ + LGYFB_deinit(); + if(g_frameReadyEvent != NULL) + { + deleteEvent(g_frameReadyEvent); // gbaGfxHandler() will automatically terminate. + g_frameReadyEvent = NULL; + } + LGY_deinit(); +}