/* Copyright 2021 flyinghead This file is part of Flycast. Flycast 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 2 of the License, or (at your option) any later version. Flycast 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 Flycast. If not, see . */ #include "emulator.h" #include "types.h" #include "stdclass.h" #include "cfg/option.h" #include "hw/aica/aica_if.h" #include "imgread/common.h" #include "hw/naomi/naomi_cart.h" #include "reios/reios.h" #include "hw/sh4/modules/mmu.h" #include "hw/sh4/sh4_if.h" #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_sched.h" #include "hw/flashrom/nvmem.h" #include "cheats.h" #include "audio/audiostream.h" #include "debug/gdb_server.h" #include "hw/pvr/Renderer_if.h" #include "hw/arm7/arm7_rec.h" #include "network/ggpo.h" #include "hw/mem/mem_watch.h" #include "network/net_handshake.h" #include "network/naomi_network.h" #include "serialize.h" #include "hw/pvr/pvr.h" #include "profiler/fc_profiler.h" #include "oslib/storage.h" #include "wsi/context.h" #include #ifndef LIBRETRO #include "ui/gui.h" #endif #include "hw/sh4/sh4_interpreter.h" #include "hw/sh4/dyna/ngen.h" settings_t settings; constexpr char const *BIOS_TITLE = "Dreamcast BIOS"; static void loadSpecialSettings() { std::string& prod_id = settings.content.gameId; NOTICE_LOG(BOOT, "Game ID is [%s]", prod_id.c_str()); if (settings.platform.isConsole()) { // Tony Hawk's Pro Skater 2 if (prod_id == "T13008D 05" || prod_id == "T13006N" // Tony Hawk's Pro Skater 1 || prod_id == "T40205N" // Tony Hawk's Skateboarding || prod_id == "T40204D 50" // Skies of Arcadia || prod_id == "MK-51052" // Eternal Arcadia (JP) || prod_id == "HDR-0076" // Flag to Flag (US) || prod_id == "MK-51007" // Super Speed Racing (JP) || prod_id == "HDR-0013" // Yu Suzuki Game Works Vol. 1 || prod_id == "6108099" // L.O.L || prod_id == "T2106M" // Miss Moonlight || prod_id == "T18702M" // Tom Clancy's Rainbow Six (US) || prod_id == "T40401N" // Tom Clancy's Rainbow Six incl. Eagle Watch Missions (EU) || prod_id == "T-45001D05" // Jet Grind Radio (US) || prod_id == "MK-51058" // JSR (JP) || prod_id == "HDR-0078" // JSR (EU) || prod_id == "MK-5105850" // Worms World Party (US) || prod_id == "T22904N" // Worms World Party (EU) || prod_id == "T7016D 50" // Shenmue (US) || prod_id == "MK-51059" // Shenmue (EU) || prod_id == "MK-5105950" // Shenmue (JP) || prod_id == "HDR-0016" // Izumo || prod_id == "T46902M" // Cardcaptor Sakura || prod_id == "HDR-0115" // Grandia II (US) || prod_id == "T17716N" // Grandia II (EU) || prod_id == "T17715D" // Grandia II (JP) || prod_id == "T4503M" // Canvas: Sepia Iro no Motif || prod_id == "T20108M" // Kimi ga Nozomu Eien || prod_id == "T47101M" // Pro Mahjong Kiwame D || prod_id == "T16801M" // Yoshia no Oka de Nekoronde... || prod_id == "T18704M" // Tamakyuu (a.k.a. Tama-cue) || prod_id == "T20133M" // Sakura Taisen 1 || prod_id == "HDR-0072" // Sakura Taisen 3 || prod_id == "HDR-0152" // Hundred Swords || prod_id == "HDR-0124" // Musapey's Choco Marker || prod_id == "T23203M" // Sister Princess Premium Edition || prod_id == "T27802M" // Sentimental Graffiti || prod_id == "T20128M" // Sentimental Graffiti 2 || prod_id == "T20104M" // Kanon || prod_id == "T20105M" // Aikagi || prod_id == "T20130M" // AIR || prod_id == "T20112M" // Cool Boarders Burrrn (JP) || prod_id == "T36901M" // Castle Fantasia - Seima Taisen (JP) || prod_id == "T46901M" // Silent Scope (US) || prod_id == "T9507N" // Silent Scope (EU) || prod_id == "T9505D" // Silent Scope (JP) || prod_id == "T9513M" // Pro Pinball - Trilogy (EU) || prod_id == "T30701D 50") { INFO_LOG(BOOT, "Enabling RTT Copy to VRAM for game %s", prod_id.c_str()); config::RenderToTextureBuffer.override(true); } // Cosmic Smash if (prod_id == "HDR-0176" || prod_id == "RDC-0057") { INFO_LOG(BOOT, "Enabling translucent depth multipass for game %s", prod_id.c_str()); config::TranslucentPolygonDepthMask.override(true); } // Extra Depth Scaling if (prod_id == "MK-51182") // NHL 2K2 { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(1e8f); } else if (prod_id == "T-8109N" // Re-Volt (US, EU, JP) || prod_id == "T8107D 50" || prod_id == "T-8101M" || prod_id == "DR001") // Sturmwind { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(100.f); } else if (prod_id == "T15110N" // Test Drive V-Rally || prod_id == "T15105D 50") { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(0.1f); } else if (prod_id == "T-8116N" // South Park Rally || prod_id == "T-8112D-50") { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(1000.f); } else if (prod_id == "T1247M") // Capcom vs. SNK - Millennium Fight 2000 Pro { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(10000.f); } std::string areas(ip_meta.area_symbols, sizeof(ip_meta.area_symbols)); bool region_usa = areas.find('U') != std::string::npos; bool region_eu = areas.find('E') != std::string::npos; bool region_japan = areas.find('J') != std::string::npos; if (region_usa || region_eu || region_japan) { switch (config::Region) { case 0: // Japan if (!region_japan) { NOTICE_LOG(BOOT, "Japan region not supported. Using %s instead", region_usa ? "USA" : "Europe"); config::Region.override(region_usa ? 1 : 2); } break; case 1: // USA if (!region_usa) { NOTICE_LOG(BOOT, "USA region not supported. Using %s instead", region_eu ? "Europe" : "Japan"); config::Region.override(region_eu ? 2 : 0); } break; case 2: // Europe if (!region_eu) { NOTICE_LOG(BOOT, "Europe region not supported. Using %s instead", region_usa ? "USA" : "Japan"); config::Region.override(region_usa ? 1 : 0); } break; case 3: // Default if (region_usa) config::Region.override(1); else if (region_eu) config::Region.override(2); else config::Region.override(0); break; } } else WARN_LOG(BOOT, "No region specified in IP.BIN"); if (config::Cable <= 1 && (!ip_meta.supportsVGA() || prod_id == "T-12504N" // Caesar's Palace (NTSC) || prod_id == "12502D-50")) // Caesar's Palace (PAL) { NOTICE_LOG(BOOT, "Game doesn't support VGA. Using TV Composite instead"); config::Cable.override(3); } if (config::Cable == 2 && (prod_id == "T40602N" // Centipede || prod_id == "T9710N" // Gauntlet Legends (US) || prod_id == "MK-51152" // World Series Baseball 2K2 || prod_id == "T-9701N" // Mortal Kombat Gold (US) || prod_id == "T1203N" // Street Fighter Alpha 3 (US) || prod_id == "T1203M" // Street Fighter Zero 3 (JP) || prod_id == "T13002N" // Vigilante 8 (US) || prod_id == "T13003N" // Toy Story 2 (US) || prod_id == "T1209N" // Gigawing (US) || prod_id == "T1208M" // Gigawing (JP) || prod_id == "T1235M" // Vampire Chronicle for Matching Service || prod_id == "T22901N" // Roadsters (US) || prod_id == "T28202M"))// Shin Nihon Pro Wrestling 4 { NOTICE_LOG(BOOT, "Game doesn't support RGB. Using TV Composite instead"); config::Cable.override(3); } if (prod_id == "T7001D 50" // Jimmy White's 2 Cueball || prod_id == "T40505D 50" // Railroad Tycoon 2 (EU) || prod_id == "T18702M" // Miss Moonlight || prod_id == "T0019M" // KenJu Atomiswave DC Conversion || prod_id == "T0020M" // Force Five Atomiswave DC Conversion || prod_id == "HDR-0187" // Fushigi no Dungeon - Fuurai no Shiren Gaiden - Onna Kenshi Asuka Kenzan! || prod_id == "T15104D 50") // Slave Zero (PAL) { NOTICE_LOG(BOOT, "Forcing real BIOS"); config::UseReios.override(false); } else if (prod_id == "T17708N" // Stupid Invaders (US) || prod_id == "T17711D" // Stupid Invaders (EU) || prod_id == "T46509M" // Suika (JP) || prod_id == "T36901M") // Cool Boarders Burrrn (JP) { NOTICE_LOG(BOOT, "Forcing HLE BIOS"); config::UseReios.override(true); } if (prod_id == "T-9707N" // San Francisco Rush 2049 (US) || prod_id == "MK-51146" // Sega Smash Pack - Volume 1 || prod_id == "T-9702D-50" // Hydro Thunder (PAL) || prod_id == "T41601N" // Elemental Gimmick Gear (US) || prod_id == "T-8116N" // South Park Rally (US) || prod_id == "T1206N") // JoJo's Bizarre Adventure (US) { NOTICE_LOG(BOOT, "Forcing NTSC broadcasting"); config::Broadcast.override(0); } else if (prod_id == "T-9709D-50" // San Francisco Rush 2049 (EU) || prod_id == "T-8112D-50" // South Park Rally (EU) || prod_id == "T7014D 50" // Super Runabout (EU) || prod_id == "T10001D 50" // MTV Sport - Skateboarding (PAL) || prod_id == "MK-5101050" // Snow Surfers || prod_id == "12502D-50") // Caesar's Palace (PAL) { NOTICE_LOG(BOOT, "Forcing PAL broadcasting"); config::Broadcast.override(1); } if (prod_id == "T1102M" // Densha de Go! 2 || prod_id == "T00000A" // The Ring of the Nibelungen (demo, hack) || prod_id == "T15124N 00" // Worms Pinball (prototype) || prod_id == "T9503M" // Eisei Meijin III || prod_id == "T5202M" // Marionette Company || prod_id == "T5301M") // World Neverland Plus { NOTICE_LOG(BOOT, "Forcing Full Framebuffer Emulation"); config::EmulateFramebuffer.override(true); } if (prod_id == "T-8102N") // TrickStyle (US) { NOTICE_LOG(BOOT, "Forcing English Language"); config::Language.override(1); } if (prod_id == "T-9701N" // Mortal Kombat (US) || prod_id == "T9701D") // Mortal Kombat (EU) { NOTICE_LOG(BOOT, "Disabling Native Depth Interpolation"); config::NativeDepthInterpolation.override(false); } // Per-pixel transparent layers int layers = 0; if (prod_id == "MK-51011" // Time Stalkers (US) || prod_id == "MK-5101153") // Time Stalkers (EU) layers = 72; else if (prod_id == "T13001N" // Blue Stinger (US) || prod_id == "HDR-0003" // Blue Stinger (JP) || prod_id == "T13001D-05" // Blue Stinger (EU) || prod_id == "T13001D 18") // Blue Stinger (DE) layers = 80; else if (prod_id == "T2102M" // Panzer Front || prod_id == "T-8118N" // Spirit of Speed (US) || prod_id == "T-8117D-50" // Spirit of Speed (EU) || prod_id == "T13002N" // Vigilante 8 (US) || prod_id == "T13002D") // Vigilante 8 (EU) layers = 64; else if (prod_id == "T2106M") // L.O.L. Lack of Love layers = 48; else if (prod_id == "T1212M") // Gaiamaster - Kessen! Seikioh Densetsu layers = 96; else if (prod_id == "T-9707N" // San Francisco Rush 2049 (US) || prod_id == "T-9709D-50" // San Francisco Rush 2049 (EU) || prod_id == "T17721N" // Conflict Zone (US) || prod_id == "T46604D") // Conflict Zone (EU) layers = 152; else if (prod_id == "MK-51033" // ECCO the Dolphin (US) || prod_id == "MK-5103350" // ECCO the Dolphin (EU) || prod_id == "HDR-0103") // ECCO the Dolphin (JP) layers = 96; else if (prod_id == "T40203N") // Draconus: Cult of the Wyrm layers = 80; else if (prod_id == "T40212N" // Soldier of Fortune (US) || prod_id == "T17726D 50") // Soldier of Fortune (EU) layers = 86; else if (prod_id == "T44102N") // BANG! Gunship Elite layers = 100; else if (prod_id == "T12502N" // MDK 2 (US) || prod_id == "T12501D 50") // MDK 2 (EU) layers = 200; else if (prod_id == "T9708D 50") // Army Men layers = 173; else if (prod_id == "MK-51038" // Zombie Revenge (US) || prod_id == "MK-5103850" // Zombie Revenge (EU) || prod_id == "HDR-0026" // Zombie Revenge (JP) || prod_id == "36801N" // Fighting Force 2 (US) || prod_id == "36802D 80" // Fighting Force 2 (PAL, en-fr) || prod_id == "36802D 18") // Fighting Force 2 (PAL, de) layers = 116; else if (prod_id == "T15112N") // Demolition Racer (US) layers = 44; else if (prod_id == "T1208N" // Tech Romancer (US) || prod_id == "T7009D50") // Tech Romancer (EU) layers = 56; if (layers != 0) { NOTICE_LOG(BOOT, "Forcing %d transparent layers", layers); config::PerPixelLayers.override(layers); } } else if (settings.platform.isArcade()) { if (prod_id == "COSMIC SMASH IN JAPAN") { INFO_LOG(BOOT, "Enabling translucent depth multipass for game %s", prod_id.c_str()); config::TranslucentPolygonDepthMask.override(true); } if (prod_id == "BEACH SPIKERS JAPAN" || prod_id == "CHOCO MARKER" || prod_id == "LOVE AND BERRY USA VER1.003" // lovebero || prod_id == "LOVE AND BERRY USA VER2.000") // lovebery { INFO_LOG(BOOT, "Enabling RTT Copy to VRAM for game %s", prod_id.c_str()); config::RenderToTextureBuffer.override(true); } if (prod_id == "RADIRGY NOA") { INFO_LOG(BOOT, "Disabling Free Play for game %s", prod_id.c_str()); config::ForceFreePlay.override(false); } if (prod_id == "VIRTUAL-ON ORATORIO TANGRAM") { INFO_LOG(BOOT, "Forcing Japan region for game %s", prod_id.c_str()); config::Region.override(0); } if (prod_id == "CAPCOM VS SNK PRO JAPAN") { INFO_LOG(BOOT, "Enabling Extra depth scaling for game %s", prod_id.c_str()); config::ExtraDepthScale.override(10000.f); } } } void Emulator::dc_reset(bool hard) { if (hard) { NetworkHandshake::term(); memwatch::unprotect(); memwatch::reset(); } sh4_sched_reset(hard); pvr::reset(hard); aica::reset(hard); getSh4Executor()->Reset(true); mem_Reset(hard); } static void setPlatform(int platform) { if (VRAM_SIZE != 0) addrspace::unprotectVram(0, VRAM_SIZE); elan::ERAM_SIZE = 0; switch (platform) { case DC_PLATFORM_DREAMCAST: settings.platform.ram_size = config::RamMod32MB ? 32_MB : 16_MB; settings.platform.vram_size = 8_MB; settings.platform.aram_size = 2_MB; settings.platform.bios_size = 2_MB; settings.platform.flash_size = 128_KB; break; case DC_PLATFORM_NAOMI: settings.platform.ram_size = 32_MB; settings.platform.vram_size = 16_MB; settings.platform.aram_size = 8_MB; settings.platform.bios_size = 2_MB; settings.platform.flash_size = 32_KB; // battery-backed ram break; case DC_PLATFORM_NAOMI2: settings.platform.ram_size = 32_MB; settings.platform.vram_size = 16_MB; // 2x16 MB VRAM, only 16 emulated settings.platform.aram_size = 8_MB; settings.platform.bios_size = 2_MB; settings.platform.flash_size = 32_KB; // battery-backed ram elan::ERAM_SIZE = 32_MB; break; case DC_PLATFORM_ATOMISWAVE: settings.platform.ram_size = 16_MB; settings.platform.vram_size = 8_MB; settings.platform.aram_size = 2_MB; settings.platform.bios_size = 128_KB; settings.platform.flash_size = 128_KB; // sram break; case DC_PLATFORM_SYSTEMSP: settings.platform.ram_size = 32_MB; settings.platform.vram_size = 16_MB; settings.platform.aram_size = 8_MB; settings.platform.bios_size = 2_MB; settings.platform.flash_size = 128_KB; // sram break; default: die("Unsupported platform"); break; } settings.platform.system = platform; settings.platform.ram_mask = settings.platform.ram_size - 1; settings.platform.vram_mask = settings.platform.vram_size - 1; settings.platform.aram_mask = settings.platform.aram_size - 1; addrspace::initMappings(); } void Emulator::init() { if (state != Uninitialized) { verify(state == Init); return; } // Default platform setPlatform(DC_PLATFORM_DREAMCAST); libGDR_init(); pvr::init(); aica::init(); mem_Init(); reios_init(); // the recompiler may start generating code at this point and needs a fully configured machine #if FEAT_SHREC != DYNAREC_NONE recompiler = Get_Sh4Recompiler(); recompiler->Init(); if(config::DynarecEnabled) INFO_LOG(DYNAREC, "Using Recompiler"); else #endif INFO_LOG(INTERPRETER, "Using Interpreter"); interpreter = Get_Sh4Interpreter(); interpreter->Init(); state = Init; } Sh4Executor *Emulator::getSh4Executor() { #if FEAT_SHREC != DYNAREC_NONE if(config::DynarecEnabled) return recompiler; else #endif return interpreter; } int getGamePlatform(const std::string& filename) { if (settings.naomi.slave) // Multiboard slave return DC_PLATFORM_NAOMI; if (filename.empty()) // Dreamcast BIOS return DC_PLATFORM_DREAMCAST; std::string extension = get_file_extension(filename); if (extension.empty()) return DC_PLATFORM_DREAMCAST; // unknown if (extension == "zip" || extension == "7z") return naomi_cart_GetPlatform(filename.c_str()); if (extension == "bin" || extension == "dat" || extension == "lst") return DC_PLATFORM_NAOMI; return DC_PLATFORM_DREAMCAST; } void Emulator::loadGame(const char *path, LoadProgress *progress) { init(); try { DEBUG_LOG(BOOT, "Loading game %s", path == nullptr ? "(nil)" : path); if (path != nullptr && strlen(path) > 0) { settings.content.path = path; if (settings.naomi.slave) { settings.content.fileName = path; } else { hostfs::FileInfo info = hostfs::storage().getFileInfo(settings.content.path); settings.content.fileName = info.name; if (settings.content.title.empty()) settings.content.title = get_file_basename(info.name); } } else { settings.content.path.clear(); settings.content.fileName.clear(); } setPlatform(getGamePlatform(settings.content.fileName)); mem_map_default(); config::Settings::instance().reset(); config::Settings::instance().load(false); dc_reset(true); memset(&settings.network.md5, 0, sizeof(settings.network.md5)); if (settings.platform.isConsole()) { if (settings.content.path.empty()) { // Boot BIOS if (!nvmem::loadFiles()) throw FlycastException("No BIOS file found in " + hostfs::getFlashSavePath("", "")); gdr::initDrive(""); } else { std::string extension = get_file_extension(settings.content.path); if (extension != "elf") { if (gdr::initDrive(settings.content.path)) { loadGameSpecificSettings(); if (config::UseReios || !nvmem::loadFiles()) { nvmem::loadHle(); NOTICE_LOG(BOOT, "Did not load BIOS, using reios"); if (!config::UseReios && config::UseReios.isReadOnly()) os_notify("This game requires a real BIOS", 15000); } } else { // Content load failed. Boot the BIOS settings.content.path.clear(); if (!nvmem::loadFiles()) throw FlycastException("This media cannot be loaded"); gdr::initDrive(""); } } else { // Elf only supported with HLE BIOS nvmem::loadHle(); gdr::initDrive(""); } } if (settings.content.path.empty()) settings.content.title = BIOS_TITLE; if (progress) progress->progress = 1.0f; } else if (settings.platform.isArcade()) { nvmem::loadFiles(); naomi_cart_LoadRom(settings.content.path, settings.content.fileName, progress); loadGameSpecificSettings(); // Reload the BIOS in case a game-specific region is set naomi_cart_LoadBios(path); } if (!settings.naomi.slave) { mcfg_DestroyDevices(); mcfg_CreateDevices(); if (settings.platform.isNaomi()) // Must be done after the maple devices are created and EEPROM is accessible naomi_cart_ConfigureEEPROM(); } #ifdef USE_RACHIEVEMENTS // RA probably isn't expecting to travel back in the past so disable it if (config::GGPOEnable) config::EnableAchievements.override(false); // Hardcore mode disables all cheats, under/overclocking, load state, lua and forces dynarec on settings.raHardcoreMode = config::EnableAchievements && config::AchievementsHardcoreMode && !NaomiNetworkSupported(); #endif cheatManager.reset(settings.content.gameId); if (cheatManager.isWidescreen()) { os_notify("Widescreen cheat activated", 2000); config::ScreenStretching.override(134); // 4:3 -> 16:9 } // reload settings so that all settings can be overridden loadGameSpecificSettings(); NetworkHandshake::init(); settings.input.fastForwardMode = false; if (!settings.content.path.empty()) { #ifndef LIBRETRO if (config::GGPOEnable) dc_loadstate(-1); else if (config::AutoLoadState && !NaomiNetworkSupported() && !settings.naomi.multiboard) dc_loadstate(config::SavestateSlot); #endif } EventManager::event(Event::Start); if (progress) { #ifdef GDB_SERVER if(config::GDBWaitForConnection) progress->label = "Waiting for debugger..."; else #endif progress->label = "Starting..."; } state = Loaded; } catch (...) { state = Error; throw; } } void Emulator::runInternal() { if (singleStep) { getSh4Executor()->Step(); singleStep = false; } else if(stepRangeTo != 0) { while (Sh4cntx.pc >= stepRangeFrom && Sh4cntx.pc <= stepRangeTo) getSh4Executor()->Step(); stepRangeFrom = 0; stepRangeTo = 0; } else { do { resetRequested = false; getSh4Executor()->Run(); if (resetRequested) { nvmem::saveFiles(); dc_reset(false); if (!restartCpu()) resetRequested = false; } } while (resetRequested); } } void Emulator::unloadGame() { try { stop(); } catch (...) { } if (state == Loaded || state == Error) { #ifndef LIBRETRO if (state == Loaded && config::AutoSaveState && !settings.content.path.empty() && !settings.naomi.multiboard && !config::GGPOEnable && !NaomiNetworkSupported()) gui_saveState(false); #endif try { dc_reset(true); } catch (const FlycastException& e) { ERROR_LOG(COMMON, "%s", e.what()); } config::Settings::instance().reset(); config::Settings::instance().load(false); settings.content.path.clear(); settings.content.gameId.clear(); settings.content.fileName.clear(); settings.content.title.clear(); settings.platform.system = DC_PLATFORM_DREAMCAST; state = Init; EventManager::event(Event::Terminate); } } void Emulator::term() { unloadGame(); if (state == Init) { debugger::term(); if (interpreter != nullptr) { interpreter->Term(); delete interpreter; interpreter = nullptr; } if (recompiler != nullptr) { recompiler->Term(); delete recompiler; recompiler = nullptr; } custom_texture.Terminate(); // lr: avoid deadlock on exit (win32) reios_term(); aica::term(); pvr::term(); mem_Term(); libGDR_term(); state = Terminated; } addrspace::release(); } void Emulator::stop() { if (state != Running) return; // Avoid race condition with GGPO restarting the sh4 for a new frame if (config::GGPOEnable) NetworkHandshake::term(); { const std::lock_guard _(mutex); // must be updated after GGPO is stopped since it may run some rollback frames state = Loaded; getSh4Executor()->Stop(); } if (config::ThreadedRendering) { rend_cancel_emu_wait(); try { checkStatus(true); } catch (const FlycastException& e) { WARN_LOG(COMMON, "%s", e.what()); throw e; } nvmem::saveFiles(); EventManager::event(Event::Pause); } else { #ifdef __ANDROID__ // defer stopping audio until after the current frame is finished // normally only useful on android due to multithreading stopRequested = true; #else TermAudio(); nvmem::saveFiles(); EventManager::event(Event::Pause); #endif } } // Called on the emulator thread for soft reset void Emulator::requestReset() { resetRequested = true; if (config::GGPOEnable) NetworkHandshake::term(); getSh4Executor()->Stop(); } void loadGameSpecificSettings() { if (settings.platform.isConsole()) { reios_disk_id(); settings.content.gameId = trim_trailing_ws(std::string(ip_meta.product_number, sizeof(ip_meta.product_number))); if (settings.content.gameId.empty()) return; } // Default per-game settings loadSpecialSettings(); config::Settings::instance().setGameId(settings.content.gameId); // Reload per-game settings config::Settings::instance().load(true); if (config::GGPOEnable || settings.raHardcoreMode) config::Sh4Clock.override(200); if (settings.raHardcoreMode) { config::WidescreenGameHacks.override(false); config::DynarecEnabled.override(true); } } void Emulator::step() { // FIXME single thread is better singleStep = true; start(); stop(); } void Emulator::stepRange(u32 from, u32 to) { stepRangeFrom = from; stepRangeTo = to; start(); stop(); } void Emulator::loadstate(Deserializer& deser) { custom_texture.Terminate(); #if FEAT_AREC == DYNAREC_JIT aica::arm::recompiler::flush(); #endif mmu_flush_table(); #if FEAT_SHREC != DYNAREC_NONE bm_Reset(); #endif memwatch::unprotect(); memwatch::reset(); dc_deserialize(deser); mmu_set_state(); getSh4Executor()->ResetCache(); EventManager::event(Event::LoadState); } void Emulator::setNetworkState(bool online) { if (settings.network.online != online) { settings.network.online = online; DEBUG_LOG(NETWORK, "Network state %d", online); if (online && settings.platform.isConsole() && config::Sh4Clock != 200) { config::Sh4Clock.override(200); getSh4Executor()->ResetCache(); } EventManager::event(Event::Network); } settings.input.fastForwardMode &= !online; } void EventManager::registerEvent(Event event, Callback callback, void *param) { unregisterEvent(event, callback, param); auto& vector = callbacks[static_cast(event)]; vector.push_back(std::make_pair(callback, param)); } void EventManager::unregisterEvent(Event event, Callback callback, void *param) { auto& vector = callbacks[static_cast(event)]; auto it = std::find(vector.begin(), vector.end(), std::make_pair(callback, param)); if (it != vector.end()) vector.erase(it); } void EventManager::broadcastEvent(Event event) { auto& vector = callbacks[static_cast(event)]; for (auto& pair : vector) pair.first(event, pair.second); } void Emulator::run() { verify(state == Running); startTime = sh4_sched_now64(); renderTimeout = false; if (!singleStep && stepRangeTo == 0) getSh4Executor()->Start(); try { runInternal(); if (ggpo::active()) ggpo::nextFrame(); } catch (...) { setNetworkState(false); state = Error; getSh4Executor()->Stop(); EventManager::event(Event::Pause); throw; } } void Emulator::start() { if (state == Running) return; verify(state == Loaded); state = Running; SetMemoryHandlers(); if (config::GGPOEnable && config::ThreadedRendering) // Not supported with GGPO config::EmulateFramebuffer.override(false); setupPtyPipe(); memwatch::protect(); if (config::ThreadedRendering) { const std::lock_guard lock(mutex); getSh4Executor()->Start(); threadResult = std::async(std::launch::async, [this] { ThreadName _("Flycast-emu"); InitAudio(); try { while (state == Running || singleStep || stepRangeTo != 0) { startTime = sh4_sched_now64(); renderTimeout = false; runInternal(); if (!ggpo::nextFrame()) break; } TermAudio(); } catch (...) { setNetworkState(false); getSh4Executor()->Stop(); TermAudio(); throw; } }); } else { stopRequested = false; InitAudio(); } EventManager::event(Event::Resume); } bool Emulator::checkStatus(bool wait) { try { std::unique_lock lock(mutex); if (threadResult.valid()) { auto localResult = threadResult; lock.unlock(); if (wait) { localResult.wait(); } else { auto result = localResult.wait_for(std::chrono::seconds(0)); if (result == std::future_status::timeout) return true; } localResult.get(); } return false; } catch (...) { EventManager::event(Event::Pause); state = Error; throw; } } bool Emulator::render() { FC_PROFILE_SCOPE; if (!config::ThreadedRendering) { if (stopRequested) { stopRequested = false; TermAudio(); nvmem::saveFiles(); EventManager::event(Event::Pause); return false; } if (state != Running) return false; run(); // TODO if stopping due to a user request, no frame has been rendered return !renderTimeout; } if (!checkStatus()) return false; if (state != Running) return false; return rend_single_frame(true); // FIXME stop flag? } void Emulator::vblank() { EventManager::event(Event::VBlank); // Time out if a frame hasn't been rendered for 50 ms if (sh4_sched_now64() - startTime <= 10000000) return; renderTimeout = true; if (ggpo::active()) ggpo::endOfFrame(); else if (!config::ThreadedRendering) getSh4Executor()->Stop(); } bool Emulator::restartCpu() { const std::lock_guard _(mutex); if (state != Running) return false; getSh4Executor()->Start(); return true; } void Emulator::insertGdrom(const std::string& path) { if (settings.platform.isArcade()) return; gdr::insertDisk(path); diskChange(); } void Emulator::openGdrom() { if (settings.platform.isArcade()) return; gdr::openLid(); diskChange(); } void Emulator::diskChange() { config::Settings::instance().reset(); config::Settings::instance().load(false); if (!settings.content.path.empty()) { hostfs::FileInfo info = hostfs::storage().getFileInfo(settings.content.path); settings.content.fileName = info.name; loadGameSpecificSettings(); } else { settings.content.fileName.clear(); settings.content.gameId.clear(); settings.content.title = BIOS_TITLE; } cheatManager.reset(settings.content.gameId); if (cheatManager.isWidescreen()) config::ScreenStretching.override(134); // 4:3 -> 16:9 custom_texture.Terminate(); EventManager::event(Event::DiskChange); } Emulator emu;