diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 83d748c..b95894b 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -94,7 +94,6 @@ - @@ -1385,4 +1384,4 @@ - \ No newline at end of file + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 9bb606b..9efb725 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -3010,9 +3010,6 @@ Genesis - - Genesis - Genesis @@ -3609,4 +3606,4 @@ {6eae784e-410d-48e6-a1b4-0a5a589db250} - \ No newline at end of file + diff --git a/Core/Debugger/LuaApi.cpp b/Core/Debugger/LuaApi.cpp index ad11d5d..859e303 100644 --- a/Core/Debugger/LuaApi.cpp +++ b/Core/Debugger/LuaApi.cpp @@ -30,6 +30,7 @@ #include "Utilities/magic_enum.hpp" #include "Shared/MemoryOperationType.h" #include "Genesis/GenesisConsole.h" +#include "Genesis/GenesisVdp.h" #ifdef _MSC_VER //TODO MSVC seems to trigger this by mistake because of the macros? @@ -255,6 +256,7 @@ int LuaApi::GetLibrary(lua_State *lua) { "getLogWindowLog", LuaApi::GetLogWindowLog }, { "getGenesisVdpDebugState", LuaApi::GetGenesisVdpDebugState }, { "getGenesisVdpTrace", LuaApi::GetGenesisVdpTrace }, + { "setGenesisVdpTraceConfig", LuaApi::SetGenesisVdpTraceConfig }, { NULL,NULL } }; @@ -1393,6 +1395,61 @@ int LuaApi::GetGenesisVdpTrace(lua_State* lua) return 1; } +int LuaApi::SetGenesisVdpTraceConfig(lua_State* lua) +{ + lua_settop(lua, 2); + string kindName = luaL_checkstring(lua, 1); + + GenesisTraceBufferKind kind; + if(kindName == "sprite") { + kind = GenesisTraceBufferKind::Sprite; + } else if(kindName == "compose") { + kind = GenesisTraceBufferKind::Compose; + } else if(kindName == "scroll") { + kind = GenesisTraceBufferKind::Scroll; + } else if(kindName == "hscrollDma") { + kind = GenesisTraceBufferKind::HScrollDma; + } else { + error("Invalid or unsupported Genesis trace buffer kind"); + } + + GenesisTraceConfig config = GenesisVdp::GetDefaultTraceConfig(kind); + if(!lua_isnoneornil(lua, 2)) { + luaL_checktype(lua, 2, LUA_TTABLE); + + auto readOptionalU32 = [&](const char* field, uint32_t& value) { + lua_getfield(lua, 2, field); + if(lua_isinteger(lua, -1)) { + value = (uint32_t)lua_tointeger(lua, -1); + } + lua_pop(lua, 1); + }; + + auto readOptionalU16 = [&](const char* field, uint16_t& value) { + lua_getfield(lua, 2, field); + if(lua_isinteger(lua, -1)) { + value = (uint16_t)lua_tointeger(lua, -1); + } + lua_pop(lua, 1); + }; + + readOptionalU32("frameStart", config.FrameStart); + readOptionalU32("frameEnd", config.FrameEnd); + readOptionalU16("lineStart", config.LineStart); + readOptionalU16("lineEnd", config.LineEnd); + readOptionalU16("xStart", config.XStart); + readOptionalU16("xEnd", config.XEnd); + readOptionalU16("columnStart", config.ColumnStart); + readOptionalU16("columnEnd", config.ColumnEnd); + readOptionalU16("dstStart", config.DstStart); + readOptionalU16("dstEnd", config.DstEnd); + readOptionalU32("maxLines", config.MaxLines); + } + + lua_pushboolean(lua, GenesisVdp::ConfigureTrace(kind, config) ? 1 : 0); + return 1; +} + int LuaApi::SetState(lua_State* lua) { lua_settop(lua, 1); diff --git a/Core/Debugger/LuaApi.h b/Core/Debugger/LuaApi.h index 658e110..c20d11e 100644 --- a/Core/Debugger/LuaApi.h +++ b/Core/Debugger/LuaApi.h @@ -85,6 +85,7 @@ class LuaApi static int GetLogWindowLog(lua_State *lua); static int GetGenesisVdpDebugState(lua_State* lua); static int GetGenesisVdpTrace(lua_State* lua); + static int SetGenesisVdpTraceConfig(lua_State* lua); static int SetState(lua_State *lua); static int GetState(lua_State *lua); diff --git a/Core/Genesis/GenesisConsole.cpp b/Core/Genesis/GenesisConsole.cpp index 23747f1..4a031ca 100644 --- a/Core/Genesis/GenesisConsole.cpp +++ b/Core/Genesis/GenesisConsole.cpp @@ -134,10 +134,9 @@ GenesisConsole::~GenesisConsole() } } -void GenesisConsole::CreateBackend(GenesisCoreType coreType) +void GenesisConsole::CreateBackend() { - (void)coreType; - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { + if(_backend) { return; } @@ -225,7 +224,7 @@ LoadRomResult GenesisConsole::LoadRom(VirtualFile& romFile) if(cfg.Port2.Type == ControllerType::None) { cfg.Port2.Type = ControllerType::GenesisController; } - CreateBackend(cfg.CoreType); + CreateBackend(); // Create control manager _controlManager.reset(new GenesisControlManager(_emu, this)); @@ -259,8 +258,8 @@ LoadRomResult GenesisConsole::LoadRom(VirtualFile& romFile) _emu->RegisterMemory(MemoryType::GenesisWorkRam, const_cast(wramPtr), wramSize); } - // Register Z80 RAM if native backend - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { + // Register Z80 RAM + if(_backend) { uint32_t audioRamSize = 0; const uint8_t* audioRam = _backend->GetMemoryPointer(MemoryType::GenesisAudioRam, audioRamSize); if(audioRam && audioRamSize > 0) { @@ -306,7 +305,6 @@ void GenesisConsole::OnVideoFrame(const uint32_t* pixels, uint32_t pitch, _frameWidth = targetWidth; _frameHeight = targetHeight; - _frameCount++; uint32_t pitchPixels = pitch / sizeof(uint32_t); uint32_t needed = _frameWidth * _frameHeight; @@ -385,7 +383,7 @@ void GenesisConsole::RunFrame() _emu->ProcessEvent(EventType::EndFrame, CpuType::GenesisMain); if(!_frameBuffer.empty() && _frameWidth > 0 && _frameHeight > 0) { - RenderedFrame frame((void*)_frameBuffer.data(), _frameWidth, _frameHeight, 1.0, _frameCount); + RenderedFrame frame((void*)_frameBuffer.data(), _frameWidth, _frameHeight, 1.0, _backend->GetFrameCount()); _emu->GetVideoDecoder()->UpdateFrame(frame, false, false); } @@ -430,10 +428,7 @@ ConsoleType GenesisConsole::GetConsoleType() vector GenesisConsole::GetCpuTypes() { - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { - return { CpuType::GenesisMain, CpuType::GenesisZ80 }; - } - return { CpuType::GenesisMain }; + return { CpuType::GenesisMain, CpuType::GenesisZ80 }; } RomFormat GenesisConsole::GetRomFormat() @@ -450,7 +445,7 @@ PpuFrameInfo GenesisConsole::GetPpuFrame() { PpuFrameInfo frame = {}; frame.FirstScanline = 0; - frame.FrameCount = _frameCount; + frame.FrameCount = _backend ? _backend->GetFrameCount() : 0; frame.Width = _frameWidth; frame.Height = _frameHeight; frame.ScanlineCount = _isPAL ? 313 : 262; @@ -587,14 +582,12 @@ void GenesisConsole::Serialize(Serializer& s) vector stateVec; - if(_backend->GetCoreType() == GenesisCoreType::Native) { - uint32_t stateFormatMarker = s.IsSaving() ? NativeStateFormatMarker : 0; - SV(stateFormatMarker); - if(!s.IsSaving() && stateFormatMarker != NativeStateFormatMarker) { - // Reject legacy Genesis states that predate the native format marker. - s.SetErrorFlag(); - return; - } + uint32_t stateFormatMarker = s.IsSaving() ? NativeStateFormatMarker : 0; + SV(stateFormatMarker); + if(!s.IsSaving() && stateFormatMarker != NativeStateFormatMarker) { + // Reject legacy Genesis states that predate the native format marker. + s.SetErrorFlag(); + return; } if(s.IsSaving()) { @@ -612,26 +605,23 @@ void GenesisConsole::Serialize(Serializer& s) GenesisZ80State GenesisConsole::GetZ80DebugState() { - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { - auto* nb = static_cast(_backend.get()); - return nb->GetZ80DebugState(); + if(_backend) { + return _backend->GetZ80DebugState(); } return {}; } void GenesisConsole::SetZ80ProgramCounter(uint16_t addr) { - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { - auto* nb = static_cast(_backend.get()); - nb->SetZ80ProgramCounter(addr); + if(_backend) { + _backend->SetZ80ProgramCounter(addr); } } uint8_t GenesisConsole::GetVdpRegister(uint8_t index) const { - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { - auto* nb = static_cast(_backend.get()); - return nb->GetVdpRegister(index); + if(_backend) { + return _backend->GetVdpRegister(index); } return 0; @@ -639,9 +629,8 @@ uint8_t GenesisConsole::GetVdpRegister(uint8_t index) const uint16_t GenesisConsole::GetHVCounter() const { - if(_backend && _backend->GetCoreType() == GenesisCoreType::Native) { - auto* nb = static_cast(_backend.get()); - return nb->GetHVCounter(); + if(_backend) { + return _backend->GetHVCounter(); } return 0; diff --git a/Core/Genesis/GenesisConsole.h b/Core/Genesis/GenesisConsole.h index bc6af90..4a4cb1d 100644 --- a/Core/Genesis/GenesisConsole.h +++ b/Core/Genesis/GenesisConsole.h @@ -4,12 +4,12 @@ #include "Shared/SettingTypes.h" #include "Genesis/GenesisTypes.h" #include "Genesis/IGenesisPlatformCallbacks.h" -#include "Genesis/IGenesisCoreBackend.h" class Emulator; class VirtualFile; class BaseControlManager; class GenesisControlManager; +class GenesisNativeBackend; class BaseVideoFilter; // --------------------------------------------------------------------------- @@ -23,14 +23,13 @@ class GenesisConsole final : public IConsole, public IGenesisPlatformCallbacks static GenesisConsole* _activeConsole; Emulator* _emu = nullptr; - unique_ptr _backend; + unique_ptr _backend; unique_ptr _controlManager; // Last rendered frame (ARGB8888) vector _frameBuffer; uint32_t _frameWidth = 320; uint32_t _frameHeight = 224; - uint32_t _frameCount = 0; ConsoleRegion _region = ConsoleRegion::Ntsc; bool _isPAL = false; @@ -40,7 +39,7 @@ class GenesisConsole final : public IConsole, public IGenesisPlatformCallbacks void DetermineRegion(const string& filename, const vector& romData); void RefreshDebuggerMemoryViews(); - void CreateBackend(GenesisCoreType coreType); + void CreateBackend(); // IGenesisPlatformCallbacks ----------------------------------------------- void OnVideoFrame(const uint32_t* pixels, uint32_t pitch, diff --git a/Core/Genesis/GenesisNativeBackend.cpp b/Core/Genesis/GenesisNativeBackend.cpp index 6090bf7..ec08324 100644 --- a/Core/Genesis/GenesisNativeBackend.cpp +++ b/Core/Genesis/GenesisNativeBackend.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include +#include #include #include #include @@ -272,6 +273,10 @@ namespace static uint32_t kCpuRamTraceAddrEnd = 0xFFCFFFu; static uint32_t kCpuRamTraceMaxLines = 300000u; +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4996) +#endif static bool TryParseEnvU32AutoBase(const char* name, uint32_t minVal, uint32_t maxVal, uint32_t& outVal) { const char* raw = std::getenv(name); @@ -313,7 +318,11 @@ namespace LoadCpuRamTraceConfigFromEnv(); std::error_code fsErr; std::filesystem::create_directories(kCpuRamTraceDirectory, fsErr); - sCpuRamTraceFile = fopen(kCpuRamTracePath, "w"); + FILE* traceFile = nullptr; + traceFile = std::fopen(kCpuRamTracePath, "w"); + if(traceFile) { + sCpuRamTraceFile = traceFile; + } if(sCpuRamTraceFile) { fprintf(sCpuRamTraceFile, "# CPU work-RAM write trace\n"); fprintf(sCpuRamTraceFile, "# frameRange=%u-%u addrRange=%06X-%06X maxLines=%u\n", @@ -322,6 +331,9 @@ namespace fflush(sCpuRamTraceFile); } } +#ifdef _MSC_VER +#pragma warning(pop) +#endif static bool CpuRamTraceShouldLog(uint32_t frame, uint32_t addr) { @@ -455,15 +467,6 @@ uint16_t GenesisNativeBackend::GetHVCounter() const return _vdp.GetHVCounter(); } -// =========================================================================== -// IGenesisCoreBackend — identity -// =========================================================================== - -GenesisCoreType GenesisNativeBackend::GetCoreType() const -{ - return GenesisCoreType::Native; -} - // =========================================================================== // Cart header parsing // =========================================================================== @@ -584,41 +587,176 @@ bool GenesisNativeBackend::IsZ80BusGranted() const return _z80BusAck || !_z80Reset; } -void GenesisNativeBackend::AdvanceZ80BusArbitration(uint32_t masterClocks) +uint8_t GenesisNativeBackend::GetOpenBusByte(uint32_t address) const { - if(masterClocks == 0u) { - return; + return (address & 1u) ? (uint8_t)_openBusValue : (uint8_t)(_openBusValue >> 8); +} + +void GenesisNativeBackend::UpdateOpenBusByte(uint32_t address, uint8_t value) +{ + if(address & 1u) { + _openBusValue = (uint16_t)((_openBusValue & 0xFF00u) | value); + } else { + _openBusValue = (uint16_t)((_openBusValue & 0x00FFu) | ((uint16_t)value << 8)); } +} - // While reset is asserted, Z80 is halted and bus can be treated as granted - // when BUSREQ is asserted. +bool GenesisNativeBackend::IsRefreshFreeAccess(BusRegion region) const +{ + switch(region) { + case BusRegion::IoRegs: + case BusRegion::MemoryMode: + case BusRegion::Z80BusReq: + case BusRegion::Z80Reset: + case BusRegion::MapperRegs: + case BusRegion::Tmss: + case BusRegion::TmssCart: + case BusRegion::VdpPorts: + return true; + default: + return false; + } +} + +uint8_t GenesisNativeBackend::GetRefreshWaitStates(BusRegion region) const +{ + static constexpr uint32_t RefreshIntervalMclk = 7u * 128u; + static constexpr uint8_t RefreshDelayCycles = 2u; + + uint64_t accessClock = GetCurrentExecutionMasterClock(); + if(_refreshLastAccessClock == 0u || accessClock < _refreshLastAccessClock) { + _refreshLastAccessClock = accessClock; + return 0u; + } + + uint64_t delta = accessClock - _refreshLastAccessClock; + if(delta > 0u) { + _refreshCounterMclk = (uint32_t)std::min(_refreshCounterMclk + delta, UINT32_MAX); + } + _refreshLastAccessClock = accessClock; + + uint8_t waitStates = 0u; + if(!IsRefreshFreeAccess(region)) { + waitStates = (uint8_t)std::min(RefreshDelayCycles * (_refreshCounterMclk / RefreshIntervalMclk), 0xFFu); + } + _refreshCounterMclk %= RefreshIntervalMclk; + return waitStates; +} + +uint64_t GenesisNativeBackend::GetCurrentExecutionMasterClock() const +{ + if(_sliceMasterClocks == 0u) { + return _masterClock; + } + return _sliceStartMasterClock + GetCurrentSliceOffsetMclk(); +} + +uint32_t GenesisNativeBackend::GetRemainingZ80CycleMclk() const +{ + uint8_t remainder = (_sliceMasterClocks == 0u) ? _z80ClockRemainder : _z80SliceClockRemainder; + return remainder == 0u ? 15u : (uint32_t)(15u - remainder); +} + +void GenesisNativeBackend::UpdateZ80ArbitrationState(uint64_t accessClock) +{ if(!_z80Reset) { - _z80BusReqDelayMclk = 0; - _z80ResumeDelayMclk = 0; _z80BusAck = _z80BusRequest; + _z80BusReqDelayMclk = 0u; + _z80ResumeDelayMclk = 0u; return; } if(_z80BusRequest) { - _z80ResumeDelayMclk = 0; - if(!_z80BusAck && _z80BusReqDelayMclk > 0u) { - if(masterClocks >= _z80BusReqDelayMclk) { - _z80BusReqDelayMclk = 0; - _z80BusAck = true; - } else { - _z80BusReqDelayMclk -= (uint16_t)masterClocks; - } + if(!_z80BusAck && accessClock >= _z80BusReqReadyClock) { + _z80BusAck = true; } + _z80ResumeDelayMclk = 0u; + _z80BusReqDelayMclk = _z80BusAck ? 0u : (uint16_t)std::min(_z80BusReqReadyClock - accessClock, 0xFFFFu); } else { - _z80BusReqDelayMclk = 0; - if(_z80ResumeDelayMclk > 0u) { - if(masterClocks >= _z80ResumeDelayMclk) { - _z80ResumeDelayMclk = 0; + _z80BusAck = false; + _z80BusReqDelayMclk = 0u; + _z80ResumeDelayMclk = (accessClock >= _z80ResumeReadyClock) ? 0u : (uint16_t)std::min(_z80ResumeReadyClock - accessClock, 0xFFFFu); + } +} + +void GenesisNativeBackend::SyncZ80ToSliceOffset(uint32_t offsetMclk) +{ + if(_sliceMasterClocks == 0u) { + return; + } + + if(offsetMclk > _sliceMasterClocks) { + offsetMclk = _sliceMasterClocks; + } + + auto advanceWindow = [&](uint32_t deltaMclk, bool allowRun) { + uint32_t accum = (uint32_t)_z80SliceClockRemainder + deltaMclk; + uint32_t cycles = accum / 15u; + _z80SliceClockRemainder = (uint8_t)(accum % 15u); + if(allowRun && cycles > 0u) { + _z80.Run((int32_t)cycles); + } + _z80SliceSyncedMclk += deltaMclk; + }; + + while(_z80SliceSyncedMclk < offsetMclk) { + uint64_t currentClock = _sliceStartMasterClock + _z80SliceSyncedMclk; + UpdateZ80ArbitrationState(currentClock); + + uint64_t targetClock = _sliceStartMasterClock + offsetMclk; + bool allowRun = false; + + if(!_z80Reset) { + allowRun = false; + } else if(_z80BusRequest) { + if(!_z80BusAck) { + allowRun = true; + if(_z80BusReqReadyClock < targetClock) { + targetClock = _z80BusReqReadyClock; + } + } + } else { + if(currentClock < _z80ResumeReadyClock) { + if(_z80ResumeReadyClock < targetClock) { + targetClock = _z80ResumeReadyClock; + } } else { - _z80ResumeDelayMclk -= (uint16_t)masterClocks; + allowRun = true; } } + + if(targetClock <= currentClock) { + UpdateZ80ArbitrationState(currentClock); + if(_z80SliceSyncedMclk >= offsetMclk) { + break; + } + if(_z80BusRequest && !_z80BusAck && currentClock >= _z80BusReqReadyClock) { + continue; + } + if(!_z80BusRequest && currentClock >= _z80ResumeReadyClock) { + continue; + } + break; + } + + advanceWindow((uint32_t)(targetClock - currentClock), allowRun); } + + UpdateZ80ArbitrationState(_sliceStartMasterClock + _z80SliceSyncedMclk); +} + +void GenesisNativeBackend::SyncZ80ToCurrentExecution() +{ + if(_sliceMasterClocks == 0u) { + UpdateZ80ArbitrationState(_masterClock); + return; + } + SyncZ80ToSliceOffset(GetCurrentSliceOffsetMclk()); +} + +void GenesisNativeBackend::AdvanceZ80BusArbitration(uint32_t masterClocks) +{ + SyncZ80ToSliceOffset(masterClocks); } void GenesisNativeBackend::UpdateFrameGeometry() @@ -731,6 +869,8 @@ void GenesisNativeBackend::RunMasterClockSlice(uint32_t masterClocks) _sliceMasterClocks = masterClocks; _slice68kStartMclk = 0u; _apuSliceSyncedMclk = 0u; + _z80SliceSyncedMclk = 0u; + _z80SliceClockRemainder = _z80ClockRemainder; _execContext = ExecContext::None; _masterClock += masterClocks; @@ -766,20 +906,16 @@ void GenesisNativeBackend::RunMasterClockSlice(uint32_t masterClocks) } } - uint32_t z80Accum = (uint32_t)_z80ClockRemainder + masterClocks; - uint32_t z80Cycles = z80Accum / 15u; - _z80ClockRemainder = (uint8_t)(z80Accum % 15u); - if(z80Cycles > 0u && _z80Reset && !_z80BusAck && _z80ResumeDelayMclk == 0u) { - _execContext = ExecContext::Z80; - _z80.Run((int32_t)z80Cycles); - _execContext = ExecContext::None; - } + _execContext = ExecContext::Z80; + AdvanceZ80BusArbitration(masterClocks); + _execContext = ExecContext::None; + _z80ClockRemainder = _z80SliceClockRemainder; SyncApuToSliceOffset(masterClocks); - AdvanceZ80BusArbitration(masterClocks); _sliceMasterClocks = 0u; _slice68kStartMclk = 0u; _apuSliceSyncedMclk = 0u; + _z80SliceSyncedMclk = 0u; _execContext = ExecContext::None; } @@ -851,6 +987,10 @@ bool GenesisNativeBackend::LoadRom(const vector& romData, const char* r _z80BusAck = false; _z80BusReqDelayMclk = 0; _z80ResumeDelayMclk = 0; + _z80BusReqReadyClock = 0; + _z80ResumeReadyClock = 0; + _z80SliceSyncedMclk = 0; + _z80SliceClockRemainder = 0; _ioData[0] = _ioData[1] = _ioData[2] = 0x00; // data/control registers start cleared, with TH/TR pins configured as inputs. _ioCtrl[0] = _ioCtrl[1] = _ioCtrl[2] = 0x00u; @@ -871,6 +1011,9 @@ bool GenesisNativeBackend::LoadRom(const vector& romData, const char* r _bootStallFrames = 0; _bootInjectFrames = 0; _bootInjectCount = 0; + _openBusValue = 0xFFFFu; + _refreshCounterMclk = 0u; + _refreshLastAccessClock = 0u; // --- Region / timing --- _isPal = region && (StringUtilities::ToLower(region) == "pal"); @@ -951,10 +1094,12 @@ void GenesisNativeBackend::RunFrame() uint32_t nextVInt = _vdp.NextVIntMclk(); uint32_t nextHInt = _vdp.NextHIntMclk(); uint32_t nextVBlank = _vdp.NextVBlankFlagMclk(); + uint32_t nextDmaRelease = _vdp.Next68kBusDmaReleaseMclk(); uint32_t nextEvent = frameMclk; if(nextVInt != UINT32_MAX && nextVInt < nextEvent) nextEvent = nextVInt; if(nextHInt != UINT32_MAX && nextHInt < nextEvent) nextEvent = nextHInt; if(nextVBlank != UINT32_MAX && nextVBlank < nextEvent) nextEvent = nextVBlank; + if(nextDmaRelease != UINT32_MAX && nextDmaRelease < nextEvent) nextEvent = nextDmaRelease; if(_z80Reset) { uint32_t nextBusArb = UINT32_MAX; if(_z80BusRequest && !_z80BusAck && _z80BusReqDelayMclk > 0u) { @@ -1091,6 +1236,7 @@ const uint8_t* GenesisNativeBackend::GetSaveEeprom(uint32_t& size) bool GenesisNativeBackend::IsPAL() const { return _isPal; } double GenesisNativeBackend::GetFps() const { return _isPal ? 49.701460 : 59.922743; } +uint32_t GenesisNativeBackend::GetFrameCount() const { return _vdp.GetFrameCount(); } uint64_t GenesisNativeBackend::GetMasterClock() const { if(_execContext != ExecContext::None && _sliceMasterClocks > 0u) { @@ -1455,24 +1601,29 @@ uint8_t GenesisNativeBackend::CpuBusWaitStates(uint32_t address, bool isWrite) c if(_cpuTestBusEnabled) { return 0u; } + BusRegion region = DecodeBusRegion(address & 0x00FFFFFFu); // 68K->VDP DMA freezes the 68K bus until DMA progress frees it again. // Apply a strong per-access penalty so the CPU effectively stalls while // RunMasterClockSlice() advances DMA at the start of each slice. if(_vdp.Is68kBusDmaActive()) { return 0xFFu; } - (void)isWrite; - switch(DecodeBusRegion(address & 0x00FFFFFFu)) { + uint8_t refreshWait = GetRefreshWaitStates(region); + uint8_t waitStates = 0u; + switch(region) { case BusRegion::Z80Space: // Access to Z80 address space incurs a wait state on the 68K bus. - return 1u; + waitStates = 1u; + break; case BusRegion::VdpPorts: - // During V-blank or display-off the VDP bus is free; minimal penalty. - // During active display the 68K must wait for an external access slot. - return _vdp.IsBlanking() ? 1u : 4u; + waitStates = _vdp.GetPortWaitStates(address, isWrite); + break; default: - return 0u; + waitStates = 0u; + break; } + uint16_t totalWait = (uint16_t)waitStates + refreshWait; + return (uint8_t)std::min(totalWait, 0xFFu); } uint8_t GenesisNativeBackend::ReadBusForZ80(uint32_t physAddr) @@ -1560,13 +1711,21 @@ void GenesisNativeBackend::EepromWrite(uint32_t address, uint8_t value) uint8_t GenesisNativeBackend::ReadCartBus(uint32_t address) { address &= 0x00FFFFFFu; + auto returnRead = [&](uint8_t value) -> uint8_t { + UpdateOpenBusByte(address, value); + return value; + }; + auto returnOpenBus = [&]() -> uint8_t { + return returnRead(GetOpenBusByte(address)); + }; + switch(DecodeBusRegion(address)) { case BusRegion::Cart: { // SRAM / EEPROM window takes priority if enabled. if(_hasEeprom && _ramEnable && address >= _eepromBusStart && address <= _eepromBusEnd) { - return EepromRead(address); + return returnRead(EepromRead(address)); } if(_sramMode != SramMode::None && _ramEnable && @@ -1576,17 +1735,17 @@ uint8_t GenesisNativeBackend::ReadCartBus(uint32_t address) uint32_t off = address - _sramStart; switch(_sramMode) { case SramMode::Word: - return (off < _saveRam.size()) ? _saveRam[off] : 0xFFu; + return returnRead((off < _saveRam.size()) ? _saveRam[off] : GetOpenBusByte(address)); case SramMode::UpperByte: // Upper byte at even addresses; odd addresses open-bus. if((address & 1u) == 0u && (off >> 1) < _saveRam.size()) - return _saveRam[off >> 1]; - return 0xFFu; + return returnRead(_saveRam[off >> 1]); + return returnOpenBus(); case SramMode::LowerByte: // Lower byte at odd addresses; even addresses open-bus. if((address & 1u) == 1u && (off >> 1) < _saveRam.size()) - return _saveRam[off >> 1]; - return 0xFFu; + return returnRead(_saveRam[off >> 1]); + return returnOpenBus(); default: break; } } @@ -1594,24 +1753,25 @@ uint8_t GenesisNativeBackend::ReadCartBus(uint32_t address) // ROM — apply SSF2 bank table. uint32_t window = address >> 19; // 512 KB window index (0-7) uint32_t bank = _romBank[window]; - if(bank == 0xFFu) return 0xFFu; + if(bank == 0xFFu) return returnOpenBus(); uint32_t physAddr = (static_cast(bank) << 19) | (address & 0x7FFFFu); - return (physAddr < _rom.size()) ? _rom[physAddr] : 0xFFu; + return returnRead((physAddr < _rom.size()) ? _rom[physAddr] : GetOpenBusByte(address)); } case BusRegion::CartOverflow: // $400000-$7FFFFF is reserved in the documented map. - return 0xFFu; + return returnOpenBus(); case BusRegion::Z80Space: + SyncZ80ToCurrentExecution(); if((address & 0xFFFFFCu) == 0xA04000u) { // YM2612 register interface uses the standard 4-byte layout: // $A04000=addr0, $A04001=data0, $A04002=addr1, $A04003=data1. uint8_t part = (uint8_t)((address >> 1) & 1u); if((address & 1u) == 0u) { - return _apu.ReadYmStatus(part); + return returnRead(_apu.ReadYmStatus(part)); } - return 0x00u; + return returnRead(0x00u); } // 68K can access Z80 bus when: // 1. BUSREQ is asserted and Z80 has released the bus, or @@ -1619,69 +1779,70 @@ uint8_t GenesisNativeBackend::ReadCartBus(uint32_t address) // The latter is the common startup pattern: write Z80 program with // reset held, then release reset. if(IsZ80BusGranted()) { - return _z80.BusRead((uint16_t)(address & 0xFFFFu)); + return returnRead(_z80.BusRead((uint16_t)(address & 0xFFFFu))); } - return 0xFFu; + return returnOpenBus(); case BusRegion::IoRegs: // I/O device registers are on odd addresses ($A10001,$03,...,$1F). // Even addresses are effectively open bus for this block. - return (address & 1u) ? ReadIoRegister((address >> 1) & 0x0Fu) : 0x00u; + return (address & 1u) ? returnRead(ReadIoRegister((address >> 1) & 0x0Fu)) : returnOpenBus(); case BusRegion::MemoryMode: // $A11000 memory mode register (32X/MCD path). Not modeled here. - return 0x00u; + return returnOpenBus(); case BusRegion::Z80BusReq: { + SyncZ80ToCurrentExecution(); // Some software polls $A11101 with BTST while others read $A11100. // Mirror bit0 on both lanes for compatibility. // bit0=0 => 68K owns Z80 bus (BUSACK asserted) // bit0=1 => Z80 bus not granted uint8_t status = (uint8_t)(0xFEu | (_z80BusAck ? 0x00u : 0x01u)); - return status; + return returnRead(status); } case BusRegion::Z80Reset: // Mirror bit0 on both lanes (same rationale as Z80BusReq above). - return _z80Reset ? 0x01u : 0x00u; + return returnRead(_z80Reset ? 0x01u : 0x00u); case BusRegion::MapperRegs: { uint32_t reg = address & 0xFFu; // $A130F1 (odd) — SRAM access status; $A130F0 (even) returns 0 if(reg == 0xF1u) { - return (uint8_t)((_ramEnable ? 0x01u : 0x00u) | (_ramWritable ? 0x00u : 0x02u)); + return returnRead((uint8_t)((_ramEnable ? 0x01u : 0x00u) | (_ramWritable ? 0x00u : 0x02u))); } - if(reg == 0xF0u) return 0x00u; + if(reg == 0xF0u) return returnOpenBus(); // $A130F3,$A130F5,...,$A130FF (odd) — SSF2 bank registers (windows 1–7) if(reg >= 0xF3u && reg <= 0xFFu && (reg & 1u) == 1u) { uint32_t window = (reg - 0xF3u) / 2u + 1u; - return window < 8u ? _romBank[window] : 0x00u; + return returnRead(window < 8u ? _romBank[window] : GetOpenBusByte(address)); } - return 0x00u; + return returnOpenBus(); } case BusRegion::Tmss: // $A14000–$A14003: TMSS unlock registers — return open-bus on reads. - return 0xFFu; + return returnOpenBus(); case BusRegion::TmssCart: // $A14101 TMSS/cartridge register — not modeled, return open bus. - return 0xFFu; + return returnOpenBus(); case BusRegion::VdpPorts: { uint32_t regGroup = address & 0x1Cu; // PSG window ($C00010-$C00017) is write-only. if(regGroup == 0x10u || regGroup == 0x14u) { - return 0xFFu; + return returnOpenBus(); } - return _vdp.ReadByte(address); + return returnRead(_vdp.ReadByte(address)); } case BusRegion::WorkRam: - return _workRam[address & 0xFFFFu]; + return returnRead(_workRam[address & 0xFFFFu]); default: - return 0xFFu; + return returnOpenBus(); } } @@ -1734,24 +1895,26 @@ void GenesisNativeBackend::WriteCartBus(uint32_t address, uint8_t value) // Games typically use word writes (#$0100 / #$0000); only the high byte // should affect the latch so the trailing low-byte write does not undo it. if((address & 1u) == 0u) { + SyncZ80ToCurrentExecution(); + uint64_t accessClock = GetCurrentExecutionMasterClock(); bool request = (value & 0x01u) != 0; if(request) { _z80BusRequest = true; - _z80ResumeDelayMclk = 0; + _z80ResumeReadyClock = 0u; if(_z80Reset) { - if(!_z80BusAck && _z80BusReqDelayMclk == 0u) { - _z80BusReqDelayMclk = Z80BusReqAckDelayMclk; - } + _z80BusAck = false; + _z80BusReqReadyClock = accessClock + GetRemainingZ80CycleMclk(); } else { _z80BusAck = true; - _z80BusReqDelayMclk = 0; + _z80BusReqReadyClock = accessClock; } } else { _z80BusRequest = false; _z80BusAck = false; - _z80BusReqDelayMclk = 0; - _z80ResumeDelayMclk = _z80Reset ? Z80BusResumeDelayMclk : 0u; + _z80BusReqReadyClock = 0u; + _z80ResumeReadyClock = _z80Reset ? (accessClock + Z80BusResumeDelayMclk) : accessClock; } + UpdateZ80ArbitrationState(accessClock); MD_TRACE_BUS("A11100 write" " req=" + std::to_string(request ? 1 : 0) + " ack=" + std::to_string(_z80BusAck ? 1 : 0) + @@ -1764,6 +1927,8 @@ void GenesisNativeBackend::WriteCartBus(uint32_t address, uint8_t value) case BusRegion::Z80Reset: // Same byte-lane behavior as $A11100. if((address & 1u) == 0u) { + SyncZ80ToCurrentExecution(); + uint64_t accessClock = GetCurrentExecutionMasterClock(); bool newReset = (value & 0x01u) != 0; // On /RESET release, restart Z80 state (and APU core state in this // simplified model). On /RESET assert, keep the CPU halted. @@ -1773,19 +1938,20 @@ void GenesisNativeBackend::WriteCartBus(uint32_t address, uint8_t value) } _z80Reset = newReset; if(!_z80Reset) { - _z80BusReqDelayMclk = 0; - _z80ResumeDelayMclk = 0; _z80BusAck = _z80BusRequest; + _z80BusReqReadyClock = accessClock; + _z80ResumeReadyClock = 0u; } else if(_z80BusRequest) { _z80BusAck = false; - _z80BusReqDelayMclk = Z80BusReqAckDelayMclk; - _z80ResumeDelayMclk = 0; + _z80BusReqReadyClock = accessClock + GetRemainingZ80CycleMclk(); + _z80ResumeReadyClock = 0u; } else { _z80BusAck = false; - _z80BusReqDelayMclk = 0; - _z80ResumeDelayMclk = 0; + _z80BusReqReadyClock = 0u; + _z80ResumeReadyClock = accessClock; } + UpdateZ80ArbitrationState(accessClock); MD_TRACE_BUS("A11200 write" " reset=" + std::to_string(_z80Reset ? 1 : 0) + " req=" + std::to_string(_z80BusRequest ? 1 : 0) + @@ -1811,6 +1977,7 @@ void GenesisNativeBackend::WriteCartBus(uint32_t address, uint8_t value) } case BusRegion::Z80Space: + SyncZ80ToCurrentExecution(); if((address & 0xFFFFFCu) == 0xA04000u) { SyncApuToCurrentExecution(); uint8_t part = (uint8_t)((address >> 1) & 1u); @@ -1863,7 +2030,7 @@ void GenesisNativeBackend::WriteCartBus(uint32_t address, uint8_t value) } // =========================================================================== -// IGenesisCoreBackend — bus read / write (for debugger) +// Bus read / write (for debugger) // =========================================================================== uint8_t GenesisNativeBackend::ReadMemory(MemoryType type, uint32_t address) @@ -1996,6 +2163,9 @@ bool GenesisNativeBackend::SaveState(vector& outState) AppendValue(outState, _bootStallFrames); AppendValue(outState, _bootInjectFrames); AppendValue(outState, _bootInjectCount); + AppendValue(outState, _openBusValue); + AppendValue(outState, _refreshCounterMclk); + AppendValue(outState, _refreshLastAccessClock); // EEPROM scalar state AppendValue(outState, _hasEeprom); @@ -2077,6 +2247,9 @@ bool GenesisNativeBackend::LoadState(const vector& state) if(!ReadValue(state, offset, _bootStallFrames)) return false; if(!ReadValue(state, offset, _bootInjectFrames)) return false; if(!ReadValue(state, offset, _bootInjectCount)) return false; + if(!ReadValue(state, offset, _openBusValue)) return false; + if(!ReadValue(state, offset, _refreshCounterMclk)) return false; + if(!ReadValue(state, offset, _refreshLastAccessClock)) return false; // EEPROM scalar state if(!ReadValue(state, offset, _hasEeprom)) return false; @@ -2115,7 +2288,12 @@ bool GenesisNativeBackend::LoadState(const vector& state) _sliceMasterClocks = 0u; _slice68kStartMclk = 0u; _apuSliceSyncedMclk = 0u; + _z80SliceSyncedMclk = 0u; + _z80SliceClockRemainder = _z80ClockRemainder; + _z80BusReqReadyClock = _masterClock + _z80BusReqDelayMclk; + _z80ResumeReadyClock = _masterClock + _z80ResumeDelayMclk; _execContext = ExecContext::None; + UpdateZ80ArbitrationState(_masterClock); _cpu.GetState() = _cpuState; _cpu.SetUSP(_cpuUsp); diff --git a/Core/Genesis/GenesisNativeBackend.h b/Core/Genesis/GenesisNativeBackend.h index c381f1c..aea87b2 100644 --- a/Core/Genesis/GenesisNativeBackend.h +++ b/Core/Genesis/GenesisNativeBackend.h @@ -1,12 +1,12 @@ #pragma once #include "pch.h" -#include "Genesis/IGenesisCoreBackend.h" #include "Genesis/GenesisI2cEeprom.h" #include "Genesis/GenesisCpu68k.h" #include "Genesis/GenesisVdp.h" #include "Genesis/GenesisCpuZ80.h" #include "Genesis/APU/GenesisApu.h" +#include "Shared/MemoryType.h" class Emulator; class IGenesisPlatformCallbacks; @@ -17,7 +17,7 @@ class IGenesisPlatformCallbacks; // Native core: correct cart bus map + native M68000 interpreter. // VDP/APU are integrated with scanline-based scheduling. // --------------------------------------------------------------------------- -class GenesisNativeBackend final : public IGenesisCoreBackend +class GenesisNativeBackend final { public: // SRAM bus width exposed by the cart header. @@ -50,7 +50,7 @@ class GenesisNativeBackend final : public IGenesisCoreBackend // Save-state identity // ----------------------------------------------------------------------- static constexpr uint32_t NativeStateMagic = 0x314E444Du; // MDN1 - static constexpr uint32_t NativeStateVersion = 34; // Deferred V-blank flag timing + static constexpr uint32_t NativeStateVersion = 36; // Refresh-delay parity state // ----------------------------------------------------------------------- // Platform callbacks / emulator @@ -74,6 +74,9 @@ class GenesisNativeBackend final : public IGenesisCoreBackend vector _saveRam; bool _cpuTestBusEnabled = false; vector _cpuTestBus; + uint16_t _openBusValue = 0xFFFFu; + mutable uint32_t _refreshCounterMclk = 0; + mutable uint64_t _refreshLastAccessClock = 0; // ----------------------------------------------------------------------- // Cart / mapper state @@ -111,8 +114,8 @@ class GenesisNativeBackend final : public IGenesisCoreBackend // ----------------------------------------------------------------------- GenesisCpuZ80 _z80; GenesisApu _apu; - static constexpr uint16_t Z80BusReqAckDelayMclk = 45; // ~3 Z80 cycles - static constexpr uint16_t Z80BusResumeDelayMclk = 15; // ~1 Z80 cycle + static constexpr uint16_t Z80BusReqAckDelayMclk = 45; // ~3 Z80 cycles + static constexpr uint16_t Z80BusResumeDelayMclk = 15; // ~1 Z80 cycle // ----------------------------------------------------------------------- // Video / timing @@ -143,6 +146,10 @@ class GenesisNativeBackend final : public IGenesisCoreBackend bool _z80BusAck = false; // true when Z80 has released its bus to 68K uint16_t _z80BusReqDelayMclk = 0; uint16_t _z80ResumeDelayMclk = 0; + uint64_t _z80BusReqReadyClock = 0; + uint64_t _z80ResumeReadyClock = 0; + uint32_t _z80SliceSyncedMclk = 0; + uint8_t _z80SliceClockRemainder = 0; uint8_t _ioData[3] = { 0x7F, 0x7F, 0x7F }; uint8_t _ioCtrl[3] = { 0x00, 0x00, 0x00 }; // MD I/O registers 0x07-0x0F (TxData/RxData/Serial control). @@ -168,7 +175,16 @@ class GenesisNativeBackend final : public IGenesisCoreBackend void InitBankTable(); BusRegion DecodeBusRegion(uint32_t address) const; bool IsZ80BusGranted() const; + uint8_t GetOpenBusByte(uint32_t address) const; + void UpdateOpenBusByte(uint32_t address, uint8_t value); + bool IsRefreshFreeAccess(BusRegion region) const; + uint8_t GetRefreshWaitStates(BusRegion region) const; void AdvanceZ80BusArbitration(uint32_t masterClocks); + void SyncZ80ToSliceOffset(uint32_t offsetMclk); + void SyncZ80ToCurrentExecution(); + void UpdateZ80ArbitrationState(uint64_t accessClock); + uint64_t GetCurrentExecutionMasterClock() const; + uint32_t GetRemainingZ80CycleMclk() const; void UpdateFrameGeometry(); void DeliverPendingVdpInterrupts(); uint32_t GetCurrentSliceOffsetMclk() const; @@ -191,12 +207,13 @@ class GenesisNativeBackend final : public IGenesisCoreBackend public: GenesisNativeBackend(Emulator* emu, IGenesisPlatformCallbacks* callbacks); - ~GenesisNativeBackend() override = default; + ~GenesisNativeBackend() = default; // CPU bus access — called by GenesisCpu68k uint8_t CpuBusRead8 (uint32_t address); void CpuBusWrite8(uint32_t address, uint8_t value); uint8_t CpuBusWaitStates(uint32_t address, bool isWrite) const; + uint16_t GetOpenBusWord() const { return _openBusValue; } // Z80 ROM window access — called by GenesisCpuZ80 uint8_t ReadBusForZ80 (uint32_t physAddr); @@ -205,41 +222,41 @@ class GenesisNativeBackend final : public IGenesisCoreBackend bool RaiseVdpIrq(uint8_t level); void VdpInterruptAcknowledge(); - // IGenesisCoreBackend -------------------------------------------------- - GenesisCoreType GetCoreType() const override; + // Core interface ------------------------------------------------------- bool LoadRom(const vector& romData, const char* region, const uint8_t* saveRamData, uint32_t saveRamSize, - const uint8_t* saveEepromData, uint32_t saveEepromSize) override; + const uint8_t* saveEepromData, uint32_t saveEepromSize); - void RunFrame() override; - void SyncSaveData() override; + void RunFrame(); + void SyncSaveData(); - const uint8_t* GetMemoryPointer(MemoryType type, uint32_t& size) override; - const uint8_t* GetSaveEeprom(uint32_t& size) override; + const uint8_t* GetMemoryPointer(MemoryType type, uint32_t& size); + const uint8_t* GetSaveEeprom(uint32_t& size); - bool IsPAL() const override; - double GetFps() const override; - uint64_t GetMasterClock() const override; - uint32_t GetMasterClockRate() const override; + bool IsPAL() const; + double GetFps() const; + uint32_t GetFrameCount() const; + uint64_t GetMasterClock() const; + uint32_t GetMasterClockRate() const; - void GetCpuState(GenesisCpuState& state) const override; - void GetVdpState(GenesisVdpState& state) const override; - void GetVdpRegisters(uint8_t regs[24]) const override; - bool GetVdpDebugState(GenesisVdpDebugState& state) const override; - bool GetVdpTraceLines(GenesisTraceBufferKind kind, vector& lines) const override; - void GetFrameSize(uint32_t& width, uint32_t& height) const override; - bool GetBackendDebugState(GenesisBackendState& state) const override; + void GetCpuState(GenesisCpuState& state) const; + void GetVdpState(GenesisVdpState& state) const; + void GetVdpRegisters(uint8_t regs[24]) const; + bool GetVdpDebugState(GenesisVdpDebugState& state) const; + bool GetVdpTraceLines(GenesisTraceBufferKind kind, vector& lines) const; + void GetFrameSize(uint32_t& width, uint32_t& height) const; + bool GetBackendDebugState(GenesisBackendState& state) const; - uint8_t ReadMemory(MemoryType type, uint32_t address) override; - void WriteMemory(MemoryType type, uint32_t address, uint8_t value) override; + uint8_t ReadMemory(MemoryType type, uint32_t address); + void WriteMemory(MemoryType type, uint32_t address, uint8_t value); - bool SetProgramCounter(uint32_t address) override; - uint32_t GetInstructionSize(uint32_t address) override; - const char* DisassembleInstruction(uint32_t address) override; + bool SetProgramCounter(uint32_t address); + uint32_t GetInstructionSize(uint32_t address); + const char* DisassembleInstruction(uint32_t address); - bool SaveState(vector& outState) override; - bool LoadState(const vector& state) override; + bool SaveState(vector& outState); + bool LoadState(const vector& state); // Z80 debug interface (called from GenesisCpuZ80 via _backend pointer) void Z80ProcessInstruction(); diff --git a/Core/Genesis/GenesisTypes.h b/Core/Genesis/GenesisTypes.h index 89a0db9..e6c6af4 100644 --- a/Core/Genesis/GenesisTypes.h +++ b/Core/Genesis/GenesisTypes.h @@ -77,6 +77,21 @@ enum class GenesisTraceBufferKind : uint8_t HScrollDma }; +struct GenesisTraceConfig : public BaseState +{ + uint32_t FrameStart = 0; + uint32_t FrameEnd = 0; + uint16_t LineStart = 0; + uint16_t LineEnd = 0; + uint16_t XStart = 0; + uint16_t XEnd = 0; + uint16_t ColumnStart = 0; + uint16_t ColumnEnd = 0; + uint16_t DstStart = 0; + uint16_t DstEnd = 0; + uint32_t MaxLines = 0; +}; + static constexpr uint32_t GenesisDebugScrollBufferSize = 32; static constexpr uint32_t GenesisDebugLineBufferSize = 347; static constexpr uint32_t GenesisDebugMaxSpritesLine = 20; diff --git a/Core/Genesis/GenesisVdp.cpp b/Core/Genesis/GenesisVdp.cpp index 09e5542..1d18aa3 100644 --- a/Core/Genesis/GenesisVdp.cpp +++ b/Core/Genesis/GenesisVdp.cpp @@ -28,42 +28,13 @@ namespace { return true; } - struct GenesisLineSprite - { - uint16_t Tile = 0; - uint16_t RawX = 0; - int16_t X = 0; - uint8_t Palette = 0; - uint8_t VertCells = 1; - uint8_t HorizCells = 1; - uint8_t CellRow = 0; - uint8_t PixRow = 0; - bool Priority = false; - bool HFlip = false; - bool VFlip = false; - }; - - struct GenesisLineSpriteCell - { - uint16_t Tile = 0; - uint16_t RawX = 0; - int16_t X = 0; - uint8_t Palette = 0; - uint8_t VertCells = 1; - uint8_t ScreenCellCol = 0; - uint8_t PatternCellOffsetX = 0; - uint8_t PatternCellOffsetY = 0; - uint8_t PixRow = 0; - bool Priority = false; - bool HFlip = false; - bool VFlip = false; - }; - static vector sDmaTraceBuffer; static vector sSpriteTraceBuffer; static vector sComposeTraceBuffer; static vector sScrollTraceBuffer; static vector sHScrollDmaTraceBuffer; + static constexpr uint32_t kFifoLatencySlots = 3u; + static constexpr uint32_t kReadLatencySlots = 3u; static void AppendTraceBufferLine(vector& buffer, const char* line) { @@ -74,9 +45,9 @@ namespace { static FILE* sSpriteTraceFile = nullptr; static uint32_t sSpriteTraceLines = 0; static constexpr uint32_t kSpriteTraceFrameStartDefault = 0u; - static constexpr uint32_t kSpriteTraceFrameEndDefault = 50u; + static constexpr uint32_t kSpriteTraceFrameEndDefault = 260u; static constexpr uint16_t kSpriteTraceLineStartDefault = 0u; - static constexpr uint16_t kSpriteTraceLineEndDefault = 50u; + static constexpr uint16_t kSpriteTraceLineEndDefault = 239u; static constexpr uint32_t kSpriteTraceMaxLinesDefault = 200000u; static uint32_t kSpriteTraceFrameStart = kSpriteTraceFrameStartDefault; static uint32_t kSpriteTraceFrameEnd = kSpriteTraceFrameEndDefault; @@ -168,6 +139,10 @@ namespace { static uint16_t kHScrollDmaTraceDstEnd = kHScrollDmaTraceDstEndDefault; static uint32_t kHScrollDmaTraceMaxLines = kHScrollDmaTraceMaxLinesDefault; +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4996) +#endif static bool TryParseEnvU32(const char* name, uint32_t minVal, uint32_t maxVal, uint32_t& outVal) { const char* raw = std::getenv(name); @@ -183,6 +158,9 @@ namespace { outVal = (uint32_t)v; return true; } +#ifdef _MSC_VER +#pragma warning(pop) +#endif static void LoadTraceConfigFromEnv() { @@ -411,10 +389,14 @@ namespace { #define NOP_(s) {(uint8_t)(s), S_OP(Nop), -1} // H40 mode: 210 slots per line, 16 mclk/slot = 3360 mclk (remaining 60 mclk is HSYNC overhead). -// Slot ordering follows BlastEM's vdp_h40 with column prefetch starting at hslot 249. -// Columns 0-19 at 8 slots each = 160 column slots, plus sprite/external/hsync/border slots. +// Slot ordering follows BlastEM's H40 phony runner: +// 249-255, 0-164, 165-182, 229-248. +// In particular: +// - column 40 is still part of the active column pipeline +// - ClearLinebuf happens at hslot 163, not 159 +// - HScrollLoad happens at hslot 244, followed by more sprite render slots const GenesisVdp::SlotDescriptor GenesisVdp::kSlotTableH40[SLOT_COUNT_H40] = { - // --- Column 0 prefetch (hslots 249-255, slot indices 0-6) --- + // --- Column 0 prefetch (hslots 249-255) --- {249, S_OP(ReadMapScrollA), 0}, SPR(250), {251, S_OP(RenderMap1), -1}, @@ -422,7 +404,7 @@ const GenesisVdp::SlotDescriptor GenesisVdp::kSlotTableH40[SLOT_COUNT_H40] = { {253, S_OP(ReadMapScrollB), 0}, SPR(254), {255, S_OP(RenderMap3), -1}, - // --- Column 0 output + columns 2-19 (hslots 0-159, slot indices 7-166) --- + // --- Column 0 output + columns 2-40 (hslots 0-160) --- {0, S_OP(RenderMapOutput), 0}, CRB (2, 1), // col 2 @ hslots 1-8 CRBR(4, 9), // col 4 @ hslots 9-16 (refresh at +1) @@ -443,69 +425,76 @@ const GenesisVdp::SlotDescriptor GenesisVdp::kSlotTableH40[SLOT_COUNT_H40] = { CRBR(34, 129), // col 34 @ hslots 129-136 (refresh) CRB (36, 137), // col 36 @ hslots 137-144 CRB (38, 145), // col 38 @ hslots 145-152 - // --- End of active display, remaining slots 167-210 --- - EXT(153), EXT(154), EXT(155), EXT(156), // external slots - {157, S_OP(HScrollLoad), -1}, // load hscroll for next line - EXT(158), - {159, S_OP(ClearLinebuf), -1}, - // Sprite scan / render / border (hslots 160-182, then HSYNC jump to 229) - SPR(160), SPR(161), SPR(162), SPR(163), - SPR(164), SPR(165), SPR(166), SPR(167), - SPR(168), SPR(169), SPR(170), SPR(171), - SPR(172), SPR(173), SPR(174), SPR(175), - SPR(176), SPR(177), SPR(178), SPR(179), - SPR(180), SPR(181), SPR(182), - // After HSYNC jump: hslots 229-248 - EXT(229), EXT(230), EXT(231), EXT(232), + CRBR(40, 153), // col 40 @ hslots 153-160 (refresh) + // --- End of active display / next-line sprite prep --- + EXT(161), EXT(162), + {163, S_OP(ClearLinebuf), -1}, + {164, S_OP(SpriteRenderOnly), -1}, + // --- Line-change / early next-line sprite phase (hslots 165-182) --- + // BlastEm keeps 165/166 as render-only and does not begin SAT scan until 167. + {165, S_OP(SpriteRenderOnly), -1}, + {166, S_OP(SpriteRenderOnly), -1}, + SPR(167), SPR(168), SPR(169), SPR(170), + SPR(171), SPR(172), SPR(173), SPR(174), + SPR(175), SPR(176), SPR(177), SPR(178), + SPR(179), SPR(180), SPR(181), SPR(182), + // --- After HSYNC jump: hslots 229-248 --- + SPR(229), SPR(230), SPR(231), + EXT(232), SPR(233), SPR(234), SPR(235), SPR(236), SPR(237), SPR(238), SPR(239), SPR(240), - SPR(241), SPR(242), SPR(243), EXT(244), - EXT(245), EXT(246), EXT(247), EXT(248), + SPR(241), SPR(242), SPR(243), + {244, S_OP(HScrollLoad), -1}, + SPR(245), SPR(246), SPR(247), SPR(248), }; // H32 mode: 171 slots per line, 20 mclk/slot = 3420 mclk. const GenesisVdp::SlotDescriptor GenesisVdp::kSlotTableH32[SLOT_COUNT_H32] = { - // --- Column 0 prefetch (hslots 244-249, slot indices 0-6) --- - {244, S_OP(ReadMapScrollA), 0}, - SPR(245), - {246, S_OP(RenderMap1), -1}, - {247, S_OP(RenderMap2), -1}, - {248, S_OP(ReadMapScrollB), 0}, - SPR(249), - {250, S_OP(RenderMap3), -1}, - // --- Column 0 output + columns 2-15 (hslots 0-127, slot indices 7-134) --- - {0, S_OP(RenderMapOutput), 0}, - CRB (2, 1), // col 2 - CRBR(4, 9), // col 4 (refresh) - CRB (6, 17), // col 6 - CRB (8, 25), // col 8 - CRBR(10, 33), // col 10 (refresh) - CRB (12, 41), // col 12 - CRB (14, 49), // col 14 - CRBR(16, 57), // col 16 (refresh) - CRB (18, 65), // col 18 - CRB (20, 73), // col 20 - CRBR(22, 81), // col 22 (refresh) - CRB (24, 89), // col 24 - CRB (26, 97), // col 26 - CRBR(28, 105), // col 28 (refresh) - CRB (30, 113), // col 30 - // --- End of active display (slot indices 135-170) --- - EXT(121), EXT(122), EXT(123), EXT(124), - {125, S_OP(HScrollLoad), -1}, - EXT(126), - {127, S_OP(ClearLinebuf), -1}, - // Sprite scan / render (hslots 128-147, then HSYNC jump to 233) - SPR(128), SPR(129), SPR(130), SPR(131), - SPR(132), SPR(133), SPR(134), SPR(135), - SPR(136), SPR(137), SPR(138), SPR(139), - SPR(140), SPR(141), SPR(142), SPR(143), - SPR(144), SPR(145), SPR(146), SPR(147), - // After HSYNC jump: hslots 233-246 - EXT(233), EXT(234), EXT(235), EXT(236), - SPR(237), SPR(238), SPR(239), SPR(240), - SPR(241), SPR(242), SPR(243), EXT(244), - EXT(245), EXT(246), EXT(247), EXT(248), + // Order follows BlastEm's H32 pipeline rotated to our line origin: + // 244-255, 0-132, 133-147, 233-243. + // Column 0 prefetch / sprite phase. + {244, S_OP(Nop), -1}, + SPR(245), SPR(246), SPR(247), SPR(248), + {249, S_OP(ReadMapScrollA), 0}, + SPR(250), + {251, S_OP(RenderMap1), -1}, + {252, S_OP(RenderMap2), -1}, + {253, S_OP(ReadMapScrollB), 0}, + {254, S_OP(SpriteRender), -1}, + {255, S_OP(RenderMap3), -1}, + {0, S_OP(RenderMapOutput), 0}, + // Active columns 2-32. + CRB (2, 1), + CRB (4, 9), + CRB (6, 17), + CRBR(8, 25), + CRB (10, 33), + CRB (12, 41), + CRB (14, 49), + CRBR(16, 57), + CRB (18, 65), + CRB (20, 73), + CRB (22, 81), + CRBR(24, 89), + CRB (26, 97), + CRB (28, 105), + CRB (30, 113), + CRBR(32, 121), + // End of visible output / next-line sprite render bootstrap. + EXT(129), EXT(130), + {131, S_OP(ClearLinebuf), -1}, + {132, S_OP(SpriteRenderOnly), -1}, + {133, S_OP(SpriteRenderOnly), -1}, + {134, S_OP(SpriteRenderOnly), -1}, + // Next-line SAT scan / render phase. + SPR(135), SPR(136), SPR(137), SPR(138), SPR(139), + SPR(140), SPR(141), SPR(142), SPR(143), SPR(144), + EXT(145), + SPR(146), SPR(147), + // HSYNC jump region before wrapping back to 244. + SPR(233), SPR(234), SPR(235), SPR(236), SPR(237), + SPR(238), SPR(239), SPR(240), SPR(241), SPR(242), + EXT(243), }; #undef S_OP @@ -623,8 +612,8 @@ uint8_t GenesisVdp::VCounterValue(uint32_t scanline) const // --------------------------------------------------------------------------- uint16_t GenesisVdp::ReadHVCounter() const { - // If HV latch is enabled (R0 bit 1), return latched value - if(_reg[0] & 0x02u) { + // BlastEm only exposes the latched counter in Mode 5. + if((_reg[0] & 0x02u) && Mode5Enabled()) { return _hvLatch; } @@ -657,19 +646,64 @@ uint16_t GenesisVdp::ReadHVCounter() const void GenesisVdp::DispatchSlot(const SlotDescriptor& slot) { + _currentHSlot = slot.hslot; switch(slot.op) { case SlotOp::ReadMapScrollA: SlotReadMapScrollA(slot.column); break; case SlotOp::ExternalSlot: SlotExternalSlot(); break; - case SlotOp::RenderMap1: SlotRenderMap1(); break; - case SlotOp::RenderMap2: SlotRenderMap2(); break; + case SlotOp::RenderMap1: + SlotRenderMap1(); + // BlastEm performs extra scan-only SAT steps on the pre-column-0 + // map fetch slots. Without them the slot-0 handoff can see one less + // sprite than hardware because _sprInfoCount stops short. + if((IsH40() && slot.hslot == 251) || (!IsH40() && slot.hslot == 251)) { + SlotScanSpriteTable(); + } + break; + case SlotOp::RenderMap2: + SlotRenderMap2(); + if((IsH40() && slot.hslot == 252) || (!IsH40() && slot.hslot == 252)) { + SlotScanSpriteTable(); + } + break; case SlotOp::ReadMapScrollB: SlotReadMapScrollB(slot.column); break; case SlotOp::ReadSpriteX: SlotReadSpriteX(); break; - case SlotOp::RenderMap3: SlotRenderMap3(); break; - case SlotOp::RenderMapOutput: SlotRenderMapOutput(slot.column); break; - case SlotOp::SpriteRender: SlotSpriteRender(); break; + case SlotOp::RenderMap3: + SlotRenderMap3(); + if((IsH40() && slot.hslot == 255) || (!IsH40() && slot.hslot == 255)) { + SlotScanSpriteTable(); + } + break; + case SlotOp::RenderMapOutput: + SlotRenderMapOutput(slot.column); + break; + case SlotOp::SpriteRenderOnly: + // After the line-change slot, BlastEm is already executing the next + // V counter line. If that line is inactive, it enters PREPARING and + // stops walking sprite cells. H32 still renders the two hslots before + // the line-change boundary (131/132), which is why this guard only + // applies at LINE_CHANGE_SLOT and later. + if(!(_sprSetupTargetLine >= ActiveHeight() + && slot.hslot >= (IsH40() ? LINE_CHANGE_SLOT_H40 : LINE_CHANGE_SLOT_H32))) { + SlotSpriteRender(); + } + break; + case SlotOp::SpriteRender: + if(!(_sprSetupTargetLine >= ActiveHeight() + && slot.hslot >= (IsH40() ? LINE_CHANGE_SLOT_H40 : LINE_CHANGE_SLOT_H32))) { + SlotSpriteRender(); + SlotScanSpriteTable(); + } + break; case SlotOp::HScrollLoad: SlotHScrollLoad(); break; case SlotOp::Refresh: break; // no-op (DRAM refresh) - case SlotOp::ClearLinebuf: SlotClearLinebuf(); break; + case SlotOp::ClearLinebuf: + SlotClearLinebuf(); + // BlastEm clears the line buffer and immediately starts sprite render + // in the same slot on H32 hslot 131 and H40 hslot 163. + if((!IsH40() && slot.hslot == 131) || (IsH40() && slot.hslot == 163)) { + SlotSpriteRender(); + } + break; case SlotOp::Nop: break; } } @@ -841,9 +875,16 @@ void GenesisVdp::SlotRenderMapOutput(int16_t column) RenderTileRowCirc(_vram, _colB2, _vOffsetB, int2, _tmpBufB, (uint8_t)(_bufBOff + 8u), SCROLL_BUF_MASK); // Pipeline phase: - // column 0 is a prefetch/border phase (no active-area output in this simplified path), - // but buffer offsets still advance at the end of the slot. + // column 0 is the handoff between the previous line's sprite prefetch and the + // current line's tile pipeline. Sprite setup for the *next* line must start + // here, after the two prefetch sprite-render slots have had a chance to finish + // the current line. if(column <= 0) { + // BlastEm performs one final scan-only step at slot 0 before the handoff + // into ReadSpriteX. Keep that ordering so the next line draw list sees the + // complete SAT result rather than the pre-slot-0 snapshot. + SlotScanSpriteTable(); + BeginNextLineSpriteSetup(_scanline); _bufAOff = (_bufAOff + 16u) & SCROLL_BUF_MASK; _bufBOff = (_bufBOff + 16u) & SCROLL_BUF_MASK; return; @@ -944,50 +985,222 @@ void GenesisVdp::SlotRenderMapOutput(int16_t column) _bufBOff = (_bufBOff + 16u) & SCROLL_BUF_MASK; } +uint16_t GenesisVdp::WrapScanline(uint32_t line) const +{ + uint16_t total = TotalScanlines(); + return total ? (uint16_t)(line % total) : 0u; +} + +void GenesisVdp::LoadLineHScroll(uint16_t line) +{ + _hscrollA = GetHScrollRaw(line, true); + _hscrollB = GetHScrollRaw(line, false); + _hscrollAFine = _hscrollA & 0x0Fu; + _hscrollBFine = _hscrollB & 0x0Fu; +} + +void GenesisVdp::ResetActiveLineSlotState(uint16_t line) +{ + (void)line; + _slotCycles = IsH40() ? 16u : 20u; + _maxSpritesLine = IsH40() ? MAX_SPRITES_LINE_H40 : MAX_SPRITES_LINE_H32; + _maxDrawsLine = IsH40() ? MAX_DRAWS_H40 : MAX_DRAWS_H32; + _slotIndex = 0; + _currentHSlot = 0xFFFFu; + _bufAOff = 0; + _bufBOff = 0; + memset(_tmpBufA, 0, sizeof(_tmpBufA)); + memset(_tmpBufB, 0, sizeof(_tmpBufB)); + + ScrollTraceLog(_frameCount, _scanline, -1, + "HS_BEGIN mode=%u base=%04X hA=%04X hB=%04X fineA=%u fineB=%u", + (unsigned)(_reg[11] & 0x03u), (unsigned)HScrollBase(), + (unsigned)_hscrollA, (unsigned)_hscrollB, + (unsigned)_hscrollAFine, (unsigned)_hscrollBFine); +} + +void GenesisVdp::BeginNextLineSpriteSetup(uint16_t line) +{ + _sprSetupTargetLine = WrapScanline((uint32_t)line + 1u); + _sprCurSlot = (int8_t)_sprInfoCount; + _sprDraws = _maxSpritesLine; + _sprRenderIdx = 0; + _sprRenderCell = 0; + _sprRenderFullSpan = false; +} + +void GenesisVdp::PrimeSpriteScanForLine(uint16_t line) +{ + _sprScanTargetLine = line; + _sprInfoCount = 0; + _sprScanLink = 0; + _sprScanDone = false; + + for(uint16_t s = 0; s < (IsH40() ? 80u : 64u) && !_sprScanDone; s++) { + SlotScanSpriteTable(); + } +} + +void GenesisVdp::PrimeSpriteDrawListForLine(uint16_t line) +{ + _sprSetupTargetLine = line; + _sprCurSlot = (int8_t)_sprInfoCount; + _sprDraws = _maxSpritesLine; + _sprRenderFullSpan = false; + if(line >= ActiveHeight()) { + return; + } + + for(uint8_t s = 0; s < _maxSpritesLine; s++) { + SlotReadSpriteX(); + } +} + +void GenesisVdp::PrimeSpriteLinebufFromDrawList() +{ + memset(_linebuf, 0, sizeof(_linebuf)); + _sprRenderIdx = 0; + _sprRenderCell = 0; + _sprRenderFullSpan = false; + _sprCellBudget = (uint8_t)(ActiveWidth() / 8u); + _sprMasked = false; + + uint8_t drawCount = (uint8_t)(_maxSpritesLine - _sprDraws); + while(_sprRenderIdx < drawCount && _sprCellBudget > 0) { + SlotSpriteRender(); + } + if(_sprRenderIdx >= drawCount && drawCount < _maxSpritesLine) { + _sprCanMask = false; + } +} + +uint16_t GenesisVdp::GetSpriteSetupTraceLine() const +{ + return _sprSetupTargetLine; +} + +void GenesisVdp::GetSpriteRenderTracePosition(uint32_t& frame, uint16_t& line) const +{ + frame = _frameCount; + line = _sprSetupTargetLine; + if(_currentHSlot == 0xFFFFu) { + return; + } + + uint16_t lineChangeSlot = IsH40() ? LINE_CHANGE_SLOT_H40 : LINE_CHANGE_SLOT_H32; + if(_currentHSlot >= lineChangeSlot) { + uint32_t nextLine = (uint32_t)line + 1u; + if(nextLine >= ActiveHeight()) { + // BlastEm attributes the post-boundary sprite cells after the last + // visible line to the next frame's line 0, while the pre-boundary + // carry-over cells remain on the current frame's inactive line. + frame++; + line = 0; + } else { + line = WrapScanline(nextLine); + } + } +} + +void GenesisVdp::BootstrapSpritePipeline(uint16_t line) +{ + LoadLineHScroll(line); + + // Bootstrap the first active line after frame start or a display-off gap. + // This is the only place where we materialize a current line immediately; + // steady-state lines are prepared by the previous line's slot stream. + PrimeSpriteScanForLine(line); + PrimeSpriteDrawListForLine(line); + PrimeSpriteLinebufFromDrawList(); + + uint16_t nextLine = WrapScanline((uint32_t)line + 1u); + if(nextLine < ActiveHeight()) { + PrimeSpriteScanForLine(nextLine); + } else { + _sprScanTargetLine = nextLine; + _sprInfoCount = 0; + _sprScanLink = 0; + _sprScanDone = true; + } + _sprPipelinePrimed = true; +} + void GenesisVdp::SlotScanSpriteTable() { if(_sprScanDone) return; bool int2 = IsInterlace2(); - uint16_t maxSprites = IsH40() ? 80u : 64u; + constexpr uint16_t maxFrameSprites = 80u; + uint16_t ymask = int2 ? 0x03FFu : 0x01FFu; + uint16_t ymin = int2 ? 256u : 128u; uint16_t cellPixH = int2 ? 16u : 8u; - uint32_t line = (uint32_t)_scanline; + uint32_t line = (uint32_t)_sprScanTargetLine; - uint8_t idx = _sprScanLink; - // Read Y/link/size from the SAT cache (snapshot from previous EndLine), - // not live VRAM, so that mid-line 68K SAT rewrites don't shift the scan. - uint16_t cacheOff = (uint16_t)idx * 8u; - uint16_t w0 = ((uint16_t)_satCache[cacheOff + 0u] << 8) | _satCache[cacheOff + 1u]; - uint16_t w1 = ((uint16_t)_satCache[cacheOff + 2u] << 8) | _satCache[cacheOff + 3u]; + if(line >= (uint32_t)ActiveHeight()) { + _sprScanDone = true; + return; + } - int16_t sprY = (int16_t)(w0 & 0x01FFu) - 128; - uint8_t vertCells = (uint8_t)(((w1 >> 8) & 0x03u) + 1u); - uint8_t link = (uint8_t)(w1 & 0x7Fu); - uint16_t sprH = (uint16_t)vertCells * cellPixH; + line += 1u; + if(int2) { + line *= 2u; + if(_interlaceField) { + line++; + } + } + line = (line + ymin) & ymask; - if((int32_t)line >= sprY && (int32_t)line < (int32_t)(sprY + sprH)) { - if(_sprInfoCount >= _maxSpritesLine) { - _status |= 0x0040u; + // BlastEm can advance up to two linked SAT entries per scan step. Matching + // that throughput is important on H32, where the pre-line scan window is + // otherwise one entry short for cases like Spritesmask. + for(uint8_t step = 0; step < 2 && !_sprScanDone; step++) { + uint8_t idx = _sprScanLink; + + // Read Y/link/size from the SAT cache. VRAM writes mirror into this cache + // so mid-frame SAT updates are visible to the scan, matching BlastEm. + uint16_t cacheOff = (uint16_t)idx * 8u; + uint16_t w0 = ((uint16_t)_satCache[cacheOff + 0u] << 8) | _satCache[cacheOff + 1u]; + uint16_t w1 = ((uint16_t)_satCache[cacheOff + 2u] << 8) | _satCache[cacheOff + 3u]; + + uint16_t sprY = w0 & ymask; + uint8_t vertCells = (uint8_t)(((w1 >> 8) & 0x03u) + 1u); + uint8_t link = (uint8_t)(w1 & 0x7Fu); + uint16_t sprH = (uint16_t)vertCells * cellPixH; + + if(sprY <= line && line < (uint32_t)(sprY + sprH)) { + if(_sprInfoCount >= _maxSpritesLine) { + _status |= 0x0040u; + _sprScanDone = true; + return; + } + SpriteInfo& info = _spriteInfoList[_sprInfoCount++]; + info.index = idx; + info.y = sprY; + // Keep SAT size nibble in raw form (HHVV where each field is size-1), + // then decode with +1 when building draw entries. + info.size = (uint8_t)((w1 >> 8) & 0x0Fu); + } + + if(link == 0 || link >= maxFrameSprites) { _sprScanDone = true; return; } - SpriteInfo& info = _spriteInfoList[_sprInfoCount++]; - info.index = idx; - info.y = sprY; - // Keep SAT size nibble in raw form (HHVV where each field is size-1), - // then decode with +1 when building draw entries. - info.size = (uint8_t)((w1 >> 8) & 0x0Fu); - } + _sprScanLink = link; - if(link == 0 || link >= maxSprites) { - _sprScanDone = true; - return; + // The second linked entry in the same scan step is only available while + // there is still room in the per-line sprite list. + if(_sprInfoCount >= _maxSpritesLine) { + break; + } } - _sprScanLink = link; } void GenesisVdp::SlotReadSpriteX() { + if(_sprSetupTargetLine >= ActiveHeight()) { + return; + } + // Circular wrap: BlastEM starts at _sprCurSlot = slot_counter (from scan), // wraps at _maxSpritesLine, then processes 0..slot_counter-1. if(_sprCurSlot == (int8_t)_maxSpritesLine) { @@ -1001,6 +1214,9 @@ void GenesisVdp::SlotReadSpriteX() // Read tile/X from SAT cache (same snapshot as the Y/link scan). uint16_t cacheOff = (uint16_t)(info.index * 8u); + if(!IsH40()) { + cacheOff = (uint16_t)(((uint16_t)info.index & 0x3Fu) * 8u); + } uint16_t w2 = ((uint16_t)_satCache[cacheOff + 4u] << 8) | _satCache[cacheOff + 5u]; uint16_t w3 = ((uint16_t)_satCache[cacheOff + 6u] << 8) | _satCache[cacheOff + 7u]; @@ -1009,14 +1225,20 @@ void GenesisVdp::SlotReadSpriteX() uint8_t horizCells = (uint8_t)(((info.size >> 2) & 0x03u) + 1u); bool vflip = (w2 & 0x1000u) != 0; uint16_t tile = w2 & 0x07FFu; - - uint16_t sprRow = (uint16_t)((int16_t)_scanline - info.y); - uint8_t cellRow = (uint8_t)(sprRow / cellPixH); - uint8_t pixRow = (uint8_t)(sprRow % cellPixH); - if(vflip) cellRow = (uint8_t)(vertCells - 1u - cellRow); - if(int2) pixRow = (uint8_t)(pixRow * 2u + (_interlaceField ? 1u : 0u)); - if(vflip && !int2) pixRow = (uint8_t)(7u - pixRow); - else if(vflip && int2) pixRow = (uint8_t)(15u - pixRow); + uint16_t ymask = int2 ? 0x03FFu : 0x01FFu; + uint16_t ymin = int2 ? 256u : 128u; + uint16_t sprLine = (uint16_t)(_sprSetupTargetLine + 1u); + uint16_t sprHeight = (uint16_t)vertCells * cellPixH; + if(int2) { + sprLine = (uint16_t)(sprLine * 2u + (_interlaceField ? 1u : 0u)); + sprHeight *= 2u; + } + sprLine = (uint16_t)((sprLine + ymin) & ymask); + uint8_t row = vflip + ? (uint8_t)(((uint16_t)(info.y + sprHeight - 1u) - sprLine) & (ymask >> 4)) + : (uint8_t)((sprLine - info.y) & (ymask >> 4)); + uint8_t cellRow = (uint8_t)(row / cellPixH); + uint8_t pixRow = (uint8_t)(row % cellPixH); if(_sprDraws > 0) { _sprDraws--; @@ -1035,7 +1257,7 @@ void GenesisVdp::SlotReadSpriteX() uint16_t tileIdx = tile + firstCol * vertCells + cellRow; uint16_t patBase = int2 ? (uint16_t)(tileIdx * 64u) : (uint16_t)(tileIdx * 32u); draw.address = (uint16_t)(patBase + pixRow * 4u); - SpriteTraceLog(_frameCount, _scanline, + SpriteTraceLog(_frameCount, GetSpriteSetupTraceLine(), "SPR_SETUP sat=%u xRaw=%03X x=%d size=%ux%u tile=%03X rowCell=%u rowPix=%u hflip=%u vflip=%u palPri=%02X firstCol=%u firstTile=%03X pat=%04X", (unsigned)info.index, (unsigned)xPos, (int)draw.xPos, (unsigned)horizCells, (unsigned)vertCells, (unsigned)tile, (unsigned)cellRow, (unsigned)pixRow, @@ -1049,22 +1271,29 @@ void GenesisVdp::SlotReadSpriteX() void GenesisVdp::SlotSpriteRender() { - // Render one sprite cell into _linebuf. The active path batches sprite - // scan + X-read in BeginLine(), so re-scanning here disturbs BlastEm-style - // ordering without adding useful state. - uint8_t drawCount = (uint8_t)(_maxSpritesLine - _sprDraws); + // Render one sprite cell into _linebuf. SAT scanning advances in lockstep + // with sprite render slots from DispatchSlot(), while ReadSpriteX builds the + // next line's draw list during the active column stream. + uint8_t drawCount = _sprRenderFullSpan ? _maxSpritesLine : (uint8_t)(_maxSpritesLine - _sprDraws); if(_sprRenderIdx >= drawCount) { // (x_pos=0) continue to be processed by render_sprite_cells, and the first // phantom slot consumes FLAG_CAN_MASK. - if(drawCount < _maxSpritesLine) { - _sprCanMask = false; - } + _sprCanMask = false; return; } if(_sprCellBudget == 0) return; uint8_t drawIndex = (uint8_t)(_maxSpritesLine - 1u - _sprRenderIdx); const SpriteDraw& draw = _spriteDrawList[drawIndex]; + if(draw.width == 0 || draw.height == 0) { + if(draw.xPos == -128 && _sprCanMask) { + _sprMasked = true; + _sprCanMask = false; + } + _sprRenderCell = 0; + _sprRenderIdx++; + return; + } bool int2 = IsInterlace2(); uint8_t vertCells = draw.height; @@ -1098,6 +1327,7 @@ void GenesisVdp::SlotSpriteRender() // - X=0 sprite acts as a mask trigger only (never renders pixels). // - Non-zero X sprite enables mask eligibility for a later X=0 sprite. bool isMaskSprite = (draw.xPos == -128); + bool maskBefore = _sprMasked; if(isMaskSprite) { if(_sprCanMask) { _sprMasked = true; @@ -1107,7 +1337,20 @@ void GenesisVdp::SlotSpriteRender() _sprCanMask = true; } - if(SpriteTraceEnabled(_frameCount, _scanline)) { + uint32_t traceFrame = 0; + uint16_t traceLine = 0; + GetSpriteRenderTracePosition(traceFrame, traceLine); + + uint16_t lineChangeSlot = IsH40() ? LINE_CHANGE_SLOT_H40 : LINE_CHANGE_SLOT_H32; + bool duplicateBoundaryTrace = _currentHSlot != 0xFFFFu + && _currentHSlot >= lineChangeSlot + && _currentHSlot <= lineChangeSlot + 1u + && (uint32_t)_sprSetupTargetLine + 1u >= ActiveHeight(); + uint32_t boundaryTraceFrame = _frameCount; + uint16_t boundaryTraceLine = ActiveHeight(); + bool traceEnabled = SpriteTraceEnabled(traceFrame, traceLine); + bool boundaryTraceEnabled = duplicateBoundaryTrace && SpriteTraceEnabled(boundaryTraceFrame, boundaryTraceLine); + if(traceEnabled || boundaryTraceEnabled) { uint8_t b0 = _vram[(patAddr + 0u) & 0xFFFFu]; uint8_t b1 = _vram[(patAddr + 1u) & 0xFFFFu]; uint8_t b2 = _vram[(patAddr + 2u) & 0xFFFFu]; @@ -1120,13 +1363,21 @@ void GenesisVdp::SlotSpriteRender() uint8_t nib = (col & 1u) ? (b & 0x0Fu) : (b >> 4); pixSeq[px] = HexNib[nib & 0x0Fu]; } - SpriteTraceLog(_frameCount, _scanline, - "SPR_CELL sat=%u draw=%u cell=%u/%u x=%d col=%u actCol=%u rowCell=%u rowPix=%u pat=%04X expPat=%04X mismatch=%u bytes=%02X%02X%02X%02X pix=%s mask=%u prevDotOvf=%u", - (unsigned)draw.satIndex, (unsigned)drawIndex, (unsigned)cellCol, (unsigned)draw.width, - (int)screenX, (unsigned)cellCol, (unsigned)actualCol, (unsigned)draw.cellRow, (unsigned)draw.pixRow, - (unsigned)patAddr, (unsigned)expectedPatAddr, (unsigned)(patMismatch ? 1u : 0u), - (unsigned)b0, (unsigned)b1, (unsigned)b2, (unsigned)b3, pixSeq, - (unsigned)(_sprMasked ? 1u : 0u), (unsigned)(_prevLineDotOverflow ? 1u : 0u)); + auto logCell = [&](uint32_t frame, uint16_t line) { + SpriteTraceLog(frame, line, + "SPR_CELL sat=%u draw=%u cell=%u/%u x=%d col=%u actCol=%u rowCell=%u rowPix=%u pat=%04X expPat=%04X mismatch=%u bytes=%02X%02X%02X%02X pix=%s mask=%u prevDotOvf=%u", + (unsigned)draw.satIndex, (unsigned)drawIndex, (unsigned)cellCol, (unsigned)draw.width, + (int)screenX, (unsigned)cellCol, (unsigned)actualCol, (unsigned)draw.cellRow, (unsigned)draw.pixRow, + (unsigned)patAddr, (unsigned)expectedPatAddr, (unsigned)(patMismatch ? 1u : 0u), + (unsigned)b0, (unsigned)b1, (unsigned)b2, (unsigned)b3, pixSeq, + (unsigned)(maskBefore ? 1u : 0u), (unsigned)(_prevLineDotOverflow ? 1u : 0u)); + }; + if(boundaryTraceEnabled) { + logCell(boundaryTraceFrame, boundaryTraceLine); + } + if(traceEnabled) { + logCell(traceFrame, traceLine); + } } if(!isMaskSprite && !_sprMasked) { @@ -1166,8 +1417,7 @@ void GenesisVdp::FifoDrainOne() uint8_t cd = e.code & 0x0Fu; switch(cd) { case 0x01: // VRAM write - _vram[e.addr & 0xFFFFu] = (uint8_t)(e.data >> 8); - _vram[(e.addr + 1u) & 0xFFFFu] = (uint8_t)e.data; + WriteVramWord(e.addr, e.data); break; case 0x03: // CRAM write CramWrite((e.addr >> 1) & 0x3Fu, e.data); @@ -1197,7 +1447,28 @@ void GenesisVdp::FifoUpdateStatus() } } -void GenesisVdp::RunDmaSrc() +uint16_t GenesisVdp::ReadDmaBusWord(uint32_t srcByteAddr) const +{ + if(!_backend) { + return 0u; + } + + srcByteAddr &= 0x00FFFFFFu; + // Match BlastEm's current behavior for "weird" DMA source regions: + // Z80/IO space and VDP space return 0 rather than using the normal bus map. + if((srcByteAddr >= 0xA00000u && srcByteAddr < 0xB00000u) + || (srcByteAddr >= 0xC00000u && srcByteAddr <= 0xE00000u)) { + return 0u; + } + + uint32_t srcBase = srcByteAddr & ~0x1FFFFu; + uint32_t srcOffset = srcByteAddr & 0x1FFFFu; + uint8_t hi = _backend->CpuBusRead8(srcBase | srcOffset); + uint8_t lo = _backend->CpuBusRead8(srcBase | ((srcOffset + 1u) & 0x1FFFFu)); + return ((uint16_t)hi << 8) | lo; +} + +void GenesisVdp::RunDmaSrc(uint64_t accessClock) { if(_dmaType != DmaType::Bus68k || _dmaLen == 0 || !_backend) return; if(_fifoCount >= 4) return; // FIFO full — can't accept another word @@ -1205,9 +1476,7 @@ void GenesisVdp::RunDmaSrc() // Read one word from 68K bus (same logic as ExecDmaBus68k, single word) uint32_t srcBase = _dmaSrc & ~0x1FFFFu; uint32_t srcOffset = _dmaSrc & 0x1FFFFu; - uint8_t hi = _backend->CpuBusRead8(srcBase | srcOffset); - uint8_t lo = _backend->CpuBusRead8(srcBase | ((srcOffset + 1u) & 0x1FFFFu)); - uint16_t word = ((uint16_t)hi << 8) | lo; + uint16_t word = ReadDmaBusWord(srcBase | srcOffset); srcOffset = (srcOffset + 2u) & 0x1FFFFu; // Enqueue into FIFO @@ -1215,6 +1484,7 @@ void GenesisVdp::RunDmaSrc() _fifo[_fifoWrite].data = word; _fifo[_fifoWrite].addr = _dmaAddr; _fifo[_fifoWrite].code = cd; + _fifo[_fifoWrite].readyClock = accessClock + GetPortLatencyMclk(); _fifoWrite = (_fifoWrite + 1u) & 3u; _fifoCount++; FifoUpdateStatus(); @@ -1231,17 +1501,23 @@ void GenesisVdp::RunDmaSrc() _dmaType = DmaType::None; _status &= ~0x0002u; _dmaBusStartDelayMclk = 0; + _dmaBusReadyClock = 0; _dmaBusMclkRemainder = 0; } } void GenesisVdp::SlotExternalSlot() { - // Priority: (1) drain FIFO, (2) DMA fill, (3) DMA copy. - // Bus68k DMA is paced centrally by Consume68kBusDma() to avoid - // split-path behavior between scheduler and per-slot execution. - if(_fifoCount > 0) { + uint64_t slotClock = GetCurrentVdpClock(); + if(_fifoCount > 0 && _fifo[_fifoRead].readyClock <= slotClock) { FifoDrainOne(); + } + + if(_dmaType == DmaType::Bus68k && _dmaLen > 0) { + if(slotClock >= _dmaBusReadyClock && _fifoCount < 4u) { + _dmaBusStartDelayMclk = 0; + RunDmaSrc(slotClock); + } } else if(_dmaType == DmaType::VramFill && _dmaFillPend && _dmaLen > 0) { ExecDmaFill(1); } else if(_dmaType == DmaType::VramCopy && _dmaLen > 0) { @@ -1251,13 +1527,23 @@ void GenesisVdp::SlotExternalSlot() void GenesisVdp::SlotClearLinebuf() { - // Only clear sprite linebuf — NOT _compositebuf. - // _compositebuf is cleared once per line in BeginLine(); the slot-table - // ClearLinebuf fires mid-line (after tile compositing, before FlushCompositeBuf) - // so wiping it here would erase the just-rendered pixels. + // ClearLinebuf is the handoff point where the VDP starts preparing the next + // line's sprite pixels. The current line has already consumed _linebuf. memset(_linebuf, 0, sizeof(_linebuf)); _sprRenderIdx = 0; _sprRenderCell = 0; + _sprRenderFullSpan = true; + _sprCellBudget = (uint8_t)(ActiveWidth() / 8u); + _sprScanTargetLine = WrapScanline((uint32_t)_scanline + 2u); + _sprInfoCount = 0; + _sprScanLink = 0; + _sprScanDone = (_sprScanTargetLine >= ActiveHeight()); + for(uint8_t i = 0; i < _sprDraws; i++) { + SpriteDraw& draw = _spriteDrawList[i]; + // BlastEm only clears x_pos for unfilled draw slots here; stale + // width/height/tile fields are still walked as mask cells. + draw.xPos = -128; + } // _sprCanMask intentionally NOT reset here. BlastEm's FLAG_CAN_MASK carries // across lines but is consumed by "phantom" empty draw slots (x_pos=0) after // all real sprites are rendered — see SlotSpriteRender. Only FLAG_MASKED @@ -1268,10 +1554,7 @@ void GenesisVdp::SlotClearLinebuf() void GenesisVdp::SlotHScrollLoad() { uint16_t nextLine = (_scanline + 1u < (uint16_t)TotalScanlines()) ? (_scanline + 1u) : 0u; - _hscrollA = GetHScrollRaw(nextLine, true); - _hscrollB = GetHScrollRaw(nextLine, false); - _hscrollAFine = _hscrollA & 0x0Fu; - _hscrollBFine = _hscrollB & 0x0Fu; + LoadLineHScroll(nextLine); ScrollTraceLog(_frameCount, _scanline, -1, "HS_LOAD nextLine=%u mode=%u base=%04X hA=%04X hB=%04X fineA=%u fineB=%u", (unsigned)nextLine, (unsigned)(_reg[11] & 0x03u), (unsigned)HScrollBase(), @@ -1341,6 +1624,8 @@ void GenesisVdp::Reset(bool isPal) _addrReg = 0; _codeReg = 0; _readBuf = 0; + _readPending = false; + _readReadyClock = 0; _writeHi = 0; _writeHiData = false; _writeHiCtrl = false; @@ -1366,6 +1651,7 @@ void GenesisVdp::Reset(bool isPal) _dmaFillVal = 0; _dmaFillPend = false; _dmaBusStartDelayMclk = 0; + _dmaBusReadyClock = 0; _dmaBusMclkRemainder = 0; _dmaVdpMclkRemainder = 0; @@ -1386,6 +1672,7 @@ void GenesisVdp::Reset(bool isPal) _fbH = 224; _mclkPos = 0; + _frameStartMasterClock = 0; _lineBegun = false; _vintFiredFrame = false; _vblankSetMclk = UINT32_MAX; @@ -1393,6 +1680,38 @@ void GenesisVdp::Reset(bool isPal) _frameFbW = 320; _frameFbH = 224; memset(_lineBackdropMask, 0, sizeof(_lineBackdropMask)); + memset(_linebuf, 0, sizeof(_linebuf)); + memset(_compositebuf, 0, sizeof(_compositebuf)); + memset(_tmpBufA, 0, sizeof(_tmpBufA)); + memset(_tmpBufB, 0, sizeof(_tmpBufB)); + memset(_spriteInfoList, 0, sizeof(_spriteInfoList)); + memset(_spriteDrawList, 0, sizeof(_spriteDrawList)); + memset(_satCache, 0, sizeof(_satCache)); + _slotIndex = 0; + _slotCycles = 16u; + _bufAOff = 0; + _bufBOff = 0; + _hscrollA = 0; + _hscrollB = 0; + _hscrollAFine = 0; + _hscrollBFine = 0; + _sprInfoCount = 0; + _sprDraws = 0; + _sprCurSlot = 0; + _sprRenderIdx = 0; + _sprRenderCell = 0; + _sprScanLink = 0; + _sprSetupTargetLine = 0; + _sprScanTargetLine = 0; + _sprScanDone = false; + _sprCanMask = false; + _sprMasked = false; + _sprRenderFullSpan = false; + _sprPipelinePrimed = false; + _maxSpritesLine = MAX_SPRITES_LINE_H40; + _maxDrawsLine = MAX_DRAWS_H40; + _sprCellBudget = 40; + _currentHSlot = 0xFFFFu; // Sensible power-on register defaults _reg[1] = 0x04; // display disable at reset @@ -1410,11 +1729,30 @@ uint16_t GenesisVdp::VramRead(uint16_t wordAddr) const return ((uint16_t)_vram[b] << 8) | _vram[b + 1]; } +void GenesisVdp::UpdateSatCacheByte(uint16_t byteAddr, uint8_t value) +{ + uint16_t rel = (uint16_t)(byteAddr - SpriteBase()); + if(rel < SAT_CACHE_SIZE) { + _satCache[rel] = value; + } +} + +void GenesisVdp::WriteVramByte(uint16_t byteAddr, uint8_t value) +{ + _vram[byteAddr & 0xFFFFu] = value; + UpdateSatCacheByte(byteAddr, value); +} + +void GenesisVdp::WriteVramWord(uint16_t byteAddr, uint16_t value) +{ + WriteVramByte(byteAddr, (uint8_t)(value >> 8)); + WriteVramByte((uint16_t)(byteAddr + 1u), (uint8_t)value); +} + void GenesisVdp::VramWrite(uint16_t wordAddr, uint16_t value) { uint32_t b = (uint32_t)(wordAddr & 0x7FFFu) * 2u; - _vram[b] = (uint8_t)(value >> 8); - _vram[b + 1] = (uint8_t)(value); + WriteVramWord((uint16_t)b, value); } uint16_t GenesisVdp::CramRead(uint8_t idx) const @@ -1584,6 +1922,86 @@ void GenesisVdp::AdvanceDmaAddr() _addrReg = _dmaAddr; } +uint64_t GenesisVdp::GetCurrentVdpClock() const +{ + return _frameStartMasterClock + _mclkPos; +} + +uint64_t GenesisVdp::GetScheduledAccessClock() const +{ + if(_backend) { + return _backend->GetMasterClock(); + } + return GetCurrentVdpClock(); +} + +uint32_t GenesisVdp::GetPortLatencyMclk() const +{ + return (IsH40() ? 16u : 20u) * kFifoLatencySlots; +} + +bool GenesisVdp::IsBlankingAtClock(uint64_t accessClock) const +{ + uint32_t frameMclk = _mclkPos; + uint32_t frameLength = (uint32_t)TotalScanlines() * MCLKS_PER_LINE; + if(frameLength > 0u && accessClock >= _frameStartMasterClock) { + frameMclk = (uint32_t)((accessClock - _frameStartMasterClock) % frameLength); + } + return !DispEnabled() || (frameMclk / MCLKS_PER_LINE) >= (uint32_t)ActiveHeight(); +} + +uint8_t GenesisVdp::GetEffectiveFifoCount(uint64_t accessClock) const +{ + uint8_t count = 0; + for(uint8_t i = 0; i < _fifoCount; i++) { + const FifoEntry& entry = _fifo[(_fifoRead + i) & 3u]; + if(entry.readyClock > accessClock) { + count++; + } + } + return count; +} + +uint64_t GenesisVdp::GetNextReadyFifoClock(uint64_t accessClock) const +{ + for(uint8_t i = 0; i < _fifoCount; i++) { + const FifoEntry& entry = _fifo[(_fifoRead + i) & 3u]; + if(entry.readyClock > accessClock) { + return entry.readyClock; + } + } + return accessClock; +} + +void GenesisVdp::SyncPortAccessState(uint64_t accessClock) +{ + if(_readPending && accessClock >= _readReadyClock) { + _readPending = false; + PrimeBuf(); + } + + while(_fifoCount > 0 && _fifo[_fifoRead].readyClock <= accessClock) { + FifoDrainOne(); + } +} + +void GenesisVdp::StartReadPipeline(uint64_t accessClock) +{ + uint8_t cd = _codeReg & 0x0Fu; + switch(cd) { + case 0x00: + case 0x04: + case 0x08: + _readPending = true; + _readReadyClock = accessClock + (IsH40() ? 16u : 20u) * kReadLatencySlots; + break; + default: + _readPending = false; + _readReadyClock = accessClock; + break; + } +} + void GenesisVdp::PrimeBuf() { // Fill the read-ahead buffer from the current address/code target @@ -1610,6 +2028,8 @@ void GenesisVdp::PrimeBuf() void GenesisVdp::BeginOperation() { + _readPending = false; + // Check if this is a DMA start if((_codeReg & 0x20) && DmaEnabled()) { _dmaAddr = _addrReg; @@ -1622,6 +2042,7 @@ void GenesisVdp::BeginOperation() _dmaLen = (rawLen == 0u) ? 0x10000u : rawLen; _dmaFillPend = false; _dmaBusStartDelayMclk = 0; + _dmaBusReadyClock = 0; _status |= 0x0002u; // DMA busy if(_dmaTraceFile) { fprintf(_dmaTraceFile, "F%04u L%03u DMA_FILL cd=%02X dst=%04X len=%u\n", @@ -1642,6 +2063,7 @@ void GenesisVdp::BeginOperation() _dmaSrc = (uint32_t)_reg[21] | ((uint32_t)_reg[22] << 8); // source VRAM word address _dmaBusStartDelayMclk = 0; + _dmaBusReadyClock = 0; _dmaVdpMclkRemainder = 0; _status |= 0x0002u; } else { @@ -1656,6 +2078,7 @@ void GenesisVdp::BeginOperation() | ((uint32_t)(_reg[23] & 0x7Fu) << 16); _dmaSrc <<= 1; // source is in words, convert to bytes _dmaBusStartDelayMclk = dma68kStartDelayMclk; + _dmaBusReadyClock = GetScheduledAccessClock() + dma68kStartDelayMclk; _dmaBusMclkRemainder = 0; _status |= 0x0002u; if(_dmaTraceFile) { @@ -1672,8 +2095,8 @@ void GenesisVdp::BeginOperation() // 68K bus DMA is paced from the backend scheduler via Consume68kBusDma(). } } else { - // Read operation: prime buffer - PrimeBuf(); + // Reads are fetched with a small latency instead of materializing instantly. + StartReadPipeline(GetScheduledAccessClock()); } } @@ -1708,6 +2131,8 @@ void GenesisVdp::HandleControlWrite(uint16_t word) uint8_t GenesisVdp::ReadByte(uint32_t addr) { + uint64_t accessClock = GetScheduledAccessClock(); + SyncPortAccessState(accessClock); uint32_t reg = addr & 0x1Fu; bool isStatusOddRead = (reg >= 0x04u && reg <= 0x07u && (reg & 1u)); if(_statusReadLatchValid && !isStatusOddRead) { @@ -1726,8 +2151,8 @@ uint8_t GenesisVdp::ReadByte(uint32_t addr) uint8_t result = (uint8_t)(_readBuf >> shift); if((addr & 1u) == 1u) { - // Low byte — advance and refill buffer - PrimeBuf(); + // Low byte — schedule the next fetch instead of materializing it immediately. + StartReadPipeline(accessClock); } return result; } @@ -1757,7 +2182,8 @@ uint8_t GenesisVdp::ReadByte(uint32_t addr) if(fifoFull) _status |= 0x0100u; else _status &= ~0x0100u; } - uint16_t s = _status; + uint16_t openBusHi = _backend ? (uint16_t)(_backend->GetOpenBusWord() & 0xFC00u) : 0u; + uint16_t s = openBusHi | (_status & 0x03FFu); // Bit 7: V-interrupt pending — set at VBlank, cleared on status read. if(_vintPending) { s |= 0x0080u; @@ -1810,6 +2236,10 @@ uint8_t GenesisVdp::ReadByte(uint32_t addr) return (uint8_t)_debugReg; default: + if(_backend) { + uint16_t openBus = _backend->GetOpenBusWord(); + return (addr & 1u) ? (uint8_t)openBus : (uint8_t)(openBus >> 8); + } return 0xFFu; } } @@ -1835,6 +2265,8 @@ void GenesisVdp::WriteByte(uint32_t addr, uint8_t val) ? (uint16_t)(((uint16_t)_writeHi << 8) | val) : (uint16_t)(((uint16_t)0x00u << 8) | val); _writeHiData = false; + uint64_t accessClock = GetScheduledAccessClock(); + SyncPortAccessState(accessClock); uint8_t cd = _codeReg & 0x0Fu; #ifdef _DEBUG @@ -1855,34 +2287,23 @@ void GenesisVdp::WriteByte(uint32_t addr, uint8_t val) } // --- FIFO enqueue --- - // During V-blank or display-disabled, VRAM bus is free so writes - // commit immediately (no queuing needed). - uint32_t curLine = _mclkPos / MCLKS_PER_LINE; - bool inVblank = !DispEnabled() || curLine >= (uint32_t)ActiveHeight(); - if(inVblank) { - // Direct write — bypass FIFO - switch(cd) { - case 0x01: - _vram[_addrReg & 0xFFFFu] = (uint8_t)(word >> 8); - _vram[(_addrReg + 1u) & 0xFFFFu] = (uint8_t)word; - break; - case 0x03: CramWrite((_addrReg >> 1) & 0x3Fu, word); break; - case 0x05: VsramWrite((_addrReg >> 1) & 0x27u, word); break; - default: break; - } - } else { - // Active display — enqueue into FIFO - // If FIFO is full, force-drain the oldest entry (stall 68K - // would be more accurate, but this keeps the backend simple). - if(_fifoCount >= 4) { - FifoDrainOne(); - } + if(_fifoCount >= 4) { +#ifdef _DEBUG + LogDebug("[MD Native][VDP] unexpected FIFO saturation after wait-state scheduling"); +#endif + SyncPortAccessState(GetNextReadyFifoClock(accessClock)); + } + if(_fifoCount < 4) { _fifo[_fifoWrite].data = word; _fifo[_fifoWrite].addr = _addrReg; _fifo[_fifoWrite].code = cd; + _fifo[_fifoWrite].readyClock = accessClock + GetPortLatencyMclk(); _fifoWrite = (_fifoWrite + 1u) & 3u; _fifoCount++; FifoUpdateStatus(); + if(IsBlankingAtClock(accessClock)) { + SyncPortAccessState(accessClock + GetPortLatencyMclk()); + } } AdvanceAddr(); } @@ -1904,6 +2325,7 @@ void GenesisVdp::WriteByte(uint32_t addr, uint8_t val) ? (uint16_t)(((uint16_t)_writeHi << 8) | val) : (uint16_t)(((uint16_t)0x00u << 8) | val); _writeHiCtrl = false; + SyncPortAccessState(GetScheduledAccessClock()); HandleControlWrite(word); } break; @@ -1924,6 +2346,34 @@ void GenesisVdp::WriteByte(uint32_t addr, uint8_t val) } } +uint8_t GenesisVdp::GetPortWaitStates(uint32_t addr, bool isWrite) const +{ + uint64_t accessClock = GetScheduledAccessClock(); + uint8_t waitStates = IsBlankingAtClock(accessClock) ? 1u : 4u; + uint32_t reg = addr & 0x1Fu; + + if(reg <= 0x03u) { + uint64_t readyClock = accessClock; + if(isWrite) { + if(GetEffectiveFifoCount(accessClock) >= 4u) { + readyClock = GetNextReadyFifoClock(accessClock); + } + } else if(_readPending && accessClock < _readReadyClock) { + readyClock = _readReadyClock; + } + + if(readyClock > accessClock) { + uint64_t stallMclk = readyClock - accessClock; + uint8_t stallCycles = (uint8_t)((stallMclk + 6u) / 7u); + if(stallCycles > waitStates) { + waitStates = stallCycles; + } + } + } + + return waitStates; +} + // =========================================================================== // DMA // =========================================================================== @@ -1949,16 +2399,13 @@ void GenesisVdp::ExecDmaBus68k(uint32_t maxWords) while(len > 0) { uint32_t srcByteAddr = srcBase | srcOffset; - uint8_t hi = _backend->CpuBusRead8(srcByteAddr); - uint8_t lo = _backend->CpuBusRead8(srcBase | ((srcOffset + 1u) & 0x1FFFFu)); - uint16_t word = ((uint16_t)hi << 8) | lo; + uint16_t word = ReadDmaBusWord(srcByteAddr); srcOffset = (srcOffset + 2u) & 0x1FFFFu; uint16_t dstAddr = _dmaAddr; switch(cd) { case 0x01: - _vram[_dmaAddr & 0xFFFFu] = (uint8_t)(word >> 8); - _vram[(_dmaAddr + 1u) & 0xFFFFu] = (uint8_t)word; + WriteVramWord(_dmaAddr, word); break; case 0x03: CramWrite ((_dmaAddr >> 1) & 0x3Fu, word); break; case 0x05: VsramWrite((_dmaAddr >> 1) & 0x27u, word); break; @@ -1989,6 +2436,7 @@ void GenesisVdp::ExecDmaBus68k(uint32_t maxWords) _dmaType = DmaType::None; _status &= ~0x0002u; _dmaBusStartDelayMclk = 0; + _dmaBusReadyClock = 0; _dmaBusMclkRemainder = 0; if(_dmaTraceFile) { fprintf(_dmaTraceFile, "F%04u L%03u DMA_BUS68K_DONE src=%06X\n", @@ -2019,7 +2467,7 @@ void GenesisVdp::ExecDmaFill(uint32_t maxBytes) // the data-port word, matching the standard #$yy00 fill command form. uint8_t fillByte = (uint8_t)(_dmaFillVal >> 8); while(len > 0) { - _vram[_dmaAddr & 0xFFFFu] = fillByte; + WriteVramByte(_dmaAddr, fillByte); AdvanceDmaAddr(); len--; _dmaLen--; @@ -2085,7 +2533,7 @@ void GenesisVdp::ExecDmaCopy(uint32_t maxWords) while(len > 0) { uint8_t val = _vram[src & 0xFFFFu]; - _vram[_dmaAddr & 0xFFFFu] = val; + WriteVramByte(_dmaAddr, val); src++; AdvanceDmaAddr(); len--; @@ -2176,74 +2624,57 @@ uint32_t GenesisVdp::Consume68kBusDma(uint32_t masterClocks, uint32_t sliceStart return 0u; } - auto getWordPeriodMclk = [&](bool blanking) -> uint32_t { - if(blanking) { - // V-blank / display disabled — measured from BlastEm reference: - // H40: ~107 words/line → 3420/107 ≈ 32 mclk/word - // H32: ~85 words/line → 3420/85 ≈ 40 mclk/word - return IsH40() ? 32u : 40u; - } + _dmaBusMclkRemainder = 0u; + uint64_t sliceStartClock = _frameStartMasterClock + sliceStartMclk; + uint64_t sliceEndClock = sliceStartClock + masterClocks; + uint32_t line = sliceStartMclk / MCLKS_PER_LINE; + bool blanking = !DispEnabled() || line >= (uint32_t)ActiveHeight(); - // Active display: limited to external access slots only. - // H40/H32 slot tables have 14 external slots per line. - // Each Bus68K DMA word uses one external slot → ~14 words/line. - // 3420 mclk / 14 ≈ 244 mclk/word. - return 244u; - }; + if(!blanking) { + // Active display: the CPU bus is locked for the slice, while the actual + // source reads are performed by SlotExternalSlot() as the VDP advances. + uint64_t readyDelta = (_dmaBusReadyClock > sliceEndClock) ? (_dmaBusReadyClock - sliceEndClock) : 0u; + _dmaBusStartDelayMclk = (uint8_t)std::min(readyDelta, 0xFFu); + return masterClocks; + } - uint32_t budget = masterClocks + _dmaBusMclkRemainder; - uint32_t consumed = 0u; - _dmaBusMclkRemainder = 0u; + // During blanking/display-off, there is no per-slot line runner, so advance + // the DMA source path using the same FIFO timing at slot cadence. + uint32_t slotMclk = IsH40() ? 16u : 20u; + uint64_t clock = sliceStartClock; + uint64_t cpuReleaseClock = sliceEndClock; - // Bus68k DMA acquires the bus before the first transfer. - if(_dmaBusStartDelayMclk > 0u) { - uint32_t delay = (_dmaBusStartDelayMclk < budget) ? _dmaBusStartDelayMclk : budget; - _dmaBusStartDelayMclk = (uint8_t)(_dmaBusStartDelayMclk - delay); - budget -= delay; - consumed += delay; - if(_dmaBusStartDelayMclk > 0u) { - return consumed; - } - } - - uint32_t framePos = sliceStartMclk + consumed; - uint32_t sliceEnd = sliceStartMclk + masterClocks; - uint32_t phaseMclk = _dmaBusMclkRemainder; - - while(framePos < sliceEnd && _dmaLen > 0u) { - uint32_t line = framePos / MCLKS_PER_LINE; - uint32_t segEnd = std::min(sliceEnd, (line + 1u) * MCLKS_PER_LINE); - bool blanking = !DispEnabled() || line >= ActiveHeight(); - uint32_t periodMclk = getWordPeriodMclk(blanking); - uint32_t clocksRemaining = segEnd - framePos; - - while(clocksRemaining > 0u && _dmaLen > 0u) { - uint32_t clocksToWord = (phaseMclk < periodMclk) ? (periodMclk - phaseMclk) : periodMclk; - if(clocksToWord > clocksRemaining) { - phaseMclk += clocksRemaining; - framePos += clocksRemaining; - clocksRemaining = 0u; - break; - } + SyncPortAccessState(clock); + if(_dmaType != DmaType::Bus68k || _dmaLen == 0u) { + cpuReleaseClock = clock; + } - uint32_t nextPos = framePos + clocksToWord; - // Transfer one word at the paced rate. For CRAM writes, the native - // renderer is scanline-based so per-word backdrop updates are skipped - // (the updated CRAM takes effect on subsequent lines). - ExecDmaBus68k(1u); - framePos = nextPos; - clocksRemaining -= clocksToWord; - phaseMclk = 0u; - - if(_dmaLen == 0u) { - _dmaBusMclkRemainder = 0u; - return framePos - sliceStartMclk; - } + while(clock < sliceEndClock) { + if(_dmaType == DmaType::Bus68k && _dmaLen > 0u && clock >= _dmaBusReadyClock && _fifoCount < 4u) { + _dmaBusStartDelayMclk = 0u; + RunDmaSrc(clock); + } else if(_dmaType != DmaType::Bus68k && cpuReleaseClock == sliceEndClock) { + cpuReleaseClock = clock; + } + + uint64_t nextClock = std::min(clock + slotMclk, sliceEndClock); + if(nextClock == clock) { + break; + } + clock = nextClock; + SyncPortAccessState(clock); + if(_dmaType != DmaType::Bus68k && cpuReleaseClock == sliceEndClock) { + cpuReleaseClock = clock; } } - _dmaBusMclkRemainder = (uint8_t)phaseMclk; - return masterClocks; + uint64_t readyDelta = (_dmaType == DmaType::Bus68k && _dmaBusReadyClock > sliceEndClock) + ? (_dmaBusReadyClock - sliceEndClock) + : 0u; + _dmaBusStartDelayMclk = (uint8_t)std::min(readyDelta, 0xFFu); + + uint64_t consumed = (cpuReleaseClock > sliceStartClock) ? (cpuReleaseClock - sliceStartClock) : 0u; + return (uint32_t)std::min(consumed, masterClocks); } // =========================================================================== @@ -2365,424 +2796,61 @@ uint8_t GenesisVdp::FetchTilePixel(uint16_t tileBase, uint8_t row, uint8_t col) return (col & 1u) ? (byte & 0x0Fu) : (byte >> 4); } -// =========================================================================== -// Rendering — per-plane -// Pixel encoding: bit7=priority, bits5:4=palette, bits3:0=color (0=transparent) -// =========================================================================== - -void GenesisVdp::RenderPlaneB(uint16_t line, uint8_t* dst, uint16_t pixels) const +uint32_t GenesisVdp::GetVIntEventOffsetMclk() const { - bool int2 = IsInterlace2(); - uint16_t planeW = PlaneWidthTiles(); - uint16_t planeH = PlaneHeightTiles(); - uint16_t hscroll = GetHScroll(line, false); // plane B - - // In interlace mode 2 each tile is 16 rows tall; name-table rows advance - // every 16 display lines. Outside interlace: normal 8-row tiles. - uint16_t tilePixH = int2 ? 16u : 8u; - uint16_t tileRow = line / tilePixH; - uint16_t pixRow = line % tilePixH; - // In interlace mode 2 the actual row within the 16-pixel tile depends - // on which field we're in: even pixels for field 0, odd for field 1. - uint16_t intPixRow = int2 ? (uint16_t)(pixRow * 2u + (_interlaceField ? 1u : 0u)) : pixRow; - - uint16_t nameBase = ScrollBBase(); - uint32_t planePxH = (uint32_t)planeH * tilePixH; - uint32_t planePxW = (uint32_t)planeW * 8u; - - for(uint16_t x = 0; x < pixels; x++) { - uint16_t px = (uint16_t)(x - hscroll) & (uint16_t)(planePxW - 1u); - - uint16_t vscroll = GetVScroll(x >> 4, false); - uint32_t py = ((uint32_t)tileRow * tilePixH + intPixRow + vscroll) % planePxH; - - uint16_t tc = (px >> 3) & (planeW - 1u); - uint16_t tr = (uint16_t)(py / tilePixH) & (planeH - 1u); - uint16_t tpx = px & 7u; - uint16_t tpy = (uint16_t)(py % tilePixH); - - uint16_t nameAddr = nameBase + (uint16_t)((tr * planeW + tc) * 2u); - uint16_t entry = ((uint16_t)_vram[nameAddr & 0xFFFFu] << 8) - | _vram[(nameAddr + 1u) & 0xFFFFu]; - - bool pri = (entry >> 15) & 1; - uint8_t pal = (uint8_t)((entry >> 13) & 3u); - bool vflip = (entry >> 12) & 1; - bool hflip = (entry >> 11) & 1; - uint16_t tile = entry & 0x7FFu; - - uint8_t col = hflip ? (7u - tpx) : (uint8_t)tpx; - uint8_t row = vflip ? ((uint8_t)(tilePixH - 1u) - (uint8_t)tpy) : (uint8_t)tpy; - - // Interlace mode 2: each tile is 64 bytes (16 rows × 4 bytes/row) - uint16_t tileBase = int2 ? (tile * 64u) : (tile * 32u); - uint8_t pix = FetchTilePixel(tileBase, row, col); - - dst[x] = (uint8_t)((pri ? 0x80u : 0x00u) | (pal << 4) | pix); + if(IsH40()) { + // Match BlastEm's VINT scheduler for H40 mode. + return MCLKS_PER_LINE - (LINE_CHANGE_SLOT_H40 - VINT_SLOT_H40) * 16u; } -} - -void GenesisVdp::RenderPlaneA(uint16_t line, uint8_t* dst, uint16_t pixels) const -{ - bool int2 = IsInterlace2(); - uint16_t planeW = PlaneWidthTiles(); - uint16_t planeH = PlaneHeightTiles(); - uint16_t hscroll = GetHScroll(line, true); // plane A - - uint16_t tilePixH = int2 ? 16u : 8u; - uint16_t tileRow = line / tilePixH; - uint16_t pixRow = line % tilePixH; - uint16_t intPixRow = int2 ? (uint16_t)(pixRow * 2u + (_interlaceField ? 1u : 0u)) : pixRow; - - uint16_t nameBase = ScrollABase(); - uint32_t planePxH = (uint32_t)planeH * tilePixH; - uint32_t planePxW = (uint32_t)planeW * 8u; - for(uint16_t x = 0; x < pixels; x++) { - uint16_t px = (uint16_t)(x - hscroll) & (uint16_t)(planePxW - 1u); - - uint16_t vscroll = GetVScroll(x >> 4, true); - uint32_t py = ((uint32_t)tileRow * tilePixH + intPixRow + vscroll) % planePxH; - - uint16_t tc = (px >> 3) & (planeW - 1u); - uint16_t tr = (uint16_t)(py / tilePixH) & (planeH - 1u); - uint16_t tpx = px & 7u; - uint16_t tpy = (uint16_t)(py % tilePixH); - - uint16_t nameAddr = nameBase + (uint16_t)((tr * planeW + tc) * 2u); - uint16_t entry = ((uint16_t)_vram[nameAddr & 0xFFFFu] << 8) - | _vram[(nameAddr + 1u) & 0xFFFFu]; - - bool pri = (entry >> 15) & 1; - uint8_t pal = (uint8_t)((entry >> 13) & 3u); - bool vflip = (entry >> 12) & 1; - bool hflip = (entry >> 11) & 1; - uint16_t tile = entry & 0x7FFu; - - uint8_t col = hflip ? (7u - tpx) : (uint8_t)tpx; - uint8_t row = vflip ? ((uint8_t)(tilePixH - 1u) - (uint8_t)tpy) : (uint8_t)tpy; - - uint16_t tileBase = int2 ? (tile * 64u) : (tile * 32u); - uint8_t pix = FetchTilePixel(tileBase, row, col); - - dst[x] = (uint8_t)((pri ? 0x80u : 0x00u) | (pal << 4) | pix); - } + // H32 wraps from hslot 147 to 233 before reaching hslot 0. + return (VINT_SLOT_H32 + 256u - 233u + 148u - LINE_CHANGE_SLOT_H32) * 20u; } -void GenesisVdp::RenderWindow(uint16_t line, uint8_t* dst, uint16_t pixels) const +uint32_t GenesisVdp::GetFirstVBlankLineMclk() const { - bool int2 = IsInterlace2(); - uint16_t nameBase = WindowBase(); - uint16_t cellW = IsH40() ? 64u : 32u; // window table width in cells - uint16_t tilePixH = int2 ? 16u : 8u; - uint16_t winLine = line / tilePixH; - uint16_t pixRow = line % tilePixH; - uint16_t intPixRow = int2 ? (uint16_t)(pixRow * 2u + (_interlaceField ? 1u : 0u)) : pixRow; - - for(uint16_t x = 0; x < pixels; x++) { - if(!IsWindowPixel(line, x)) { - dst[x] = 0; // not window — transparent (keep plane A result) - continue; - } - - uint16_t tc = x >> 3; - uint16_t tpx = x & 7u; - - uint16_t nameAddr = nameBase + (uint16_t)((winLine * cellW + tc) * 2u); - uint16_t entry = ((uint16_t)_vram[nameAddr & 0xFFFFu] << 8) - | _vram[(nameAddr + 1u) & 0xFFFFu]; - - bool pri = (entry >> 15) & 1; - uint8_t pal = (uint8_t)((entry >> 13) & 3u); - bool vflip = (entry >> 12) & 1; - bool hflip = (entry >> 11) & 1; - uint16_t tile = entry & 0x7FFu; - - uint8_t col = hflip ? (7u - tpx) : (uint8_t)tpx; - uint8_t row = vflip ? ((uint8_t)(tilePixH - 1u) - (uint8_t)intPixRow) : (uint8_t)intPixRow; - - uint16_t tileBase = int2 ? (tile * 64u) : (tile * 32u); - uint8_t pix = FetchTilePixel(tileBase, row, col); - - // Mark window pixels with a special flag (bit 6) so compositor knows - dst[x] = (uint8_t)((pri ? 0x80u : 0x00u) | 0x40u | (pal << 4) | pix); - } + return (uint32_t)ActiveHeight() * MCLKS_PER_LINE; } -void GenesisVdp::RenderSprites(uint16_t line, uint8_t* dst, uint16_t pixels) +uint32_t GenesisVdp::GetVBlankFlagOffsetMclk() const { - bool int2 = IsInterlace2(); - uint16_t sprBase = SpriteBase(); - uint16_t maxSprites = IsH40() ? 80u : 64u; - uint16_t maxPerLine = IsH40() ? 20u : 16u; - uint16_t maxCells = IsH40() ? 40u : 32u; - uint16_t cellPixH = int2 ? 16u : 8u; - uint16_t effLine = int2 ? (uint16_t)(line * 2u + (_interlaceField ? 1u : 0u)) : line; - - GenesisLineSprite spriteList[80] = {}; - GenesisLineSpriteCell cellList[40] = {}; - uint16_t spriteCount = 0; - uint16_t cellCount = 0; - bool lineDotOverflow = false; - bool prevLineDotOverflow = _prevLineDotOverflow; - - // Build the list of sprites active on this line by traversing the SAT link chain. - uint8_t idx = 0; - for(uint16_t s = 0; s < maxSprites; s++) { - uint16_t entryBase = (uint16_t)(sprBase + (uint16_t)idx * 8u); - uint16_t w0 = ((uint16_t)_vram[(entryBase + 0u) & 0xFFFFu] << 8) | _vram[(entryBase + 1u) & 0xFFFFu]; - uint16_t w1 = ((uint16_t)_vram[(entryBase + 2u) & 0xFFFFu] << 8) | _vram[(entryBase + 3u) & 0xFFFFu]; - uint16_t w2 = ((uint16_t)_vram[(entryBase + 4u) & 0xFFFFu] << 8) | _vram[(entryBase + 5u) & 0xFFFFu]; - uint16_t w3 = ((uint16_t)_vram[(entryBase + 6u) & 0xFFFFu] << 8) | _vram[(entryBase + 7u) & 0xFFFFu]; - - int16_t sprY = (int16_t)(w0 & 0x01FFu) - 128; - uint8_t vertCells = (uint8_t)(((w1 >> 8) & 0x03u) + 1u); - uint8_t horizCells = (uint8_t)(((w1 >> 10) & 0x03u) + 1u); - uint8_t link = (uint8_t)(w1 & 0x7Fu); - uint16_t sprH = (uint16_t)vertCells * cellPixH; - - if((int16_t)effLine >= sprY && (int16_t)effLine < (int16_t)(sprY + sprH)) { - if(spriteCount >= maxPerLine) { - _status |= 0x0040u; - break; - } - - bool vflip = (w2 & 0x1000u) != 0; - uint16_t sprRow = (uint16_t)((int16_t)effLine - sprY); - uint8_t cellRow = (uint8_t)(sprRow / cellPixH); - uint8_t pixRow = (uint8_t)(sprRow % cellPixH); - - GenesisLineSprite& sprite = spriteList[spriteCount++]; - sprite.Tile = w2 & 0x07FFu; - sprite.RawX = w3 & 0x01FFu; - sprite.X = (int16_t)sprite.RawX - 128; - sprite.Palette = (uint8_t)((w2 >> 13) & 0x03u); - sprite.VertCells = vertCells; - sprite.HorizCells = horizCells; - sprite.CellRow = vflip ? (uint8_t)(vertCells - 1u - cellRow) : cellRow; - sprite.PixRow = pixRow; - sprite.Priority = (w2 & 0x8000u) != 0; - sprite.HFlip = (w2 & 0x0800u) != 0; - sprite.VFlip = vflip; - } - - if(link == 0 || link >= maxSprites) { - break; - } - idx = link; - } - - // Expand the selected sprites into individual 8x8/8x16 cells. - // Dot-overflow accounting is based on parsed sprite cells, independent of - // later masking decisions. - for(uint16_t i = 0; i < spriteCount; i++) { - const GenesisLineSprite& sprite = spriteList[i]; - - for(uint8_t screenCellCol = 0; screenCellCol < sprite.HorizCells; screenCellCol++) { - if(cellCount >= maxCells) { - _status |= 0x0040u; - lineDotOverflow = true; - break; - } - - GenesisLineSpriteCell& cell = cellList[cellCount++]; - cell.Tile = sprite.Tile; - cell.RawX = sprite.RawX; - cell.X = sprite.X; - cell.Palette = sprite.Palette; - cell.VertCells = sprite.VertCells; - cell.ScreenCellCol = screenCellCol; - cell.PatternCellOffsetX = sprite.HFlip ? (uint8_t)(sprite.HorizCells - 1u - screenCellCol) : screenCellCol; - cell.PatternCellOffsetY = sprite.CellRow; - cell.PixRow = sprite.PixRow; - cell.Priority = sprite.Priority; - cell.HFlip = sprite.HFlip; - cell.VFlip = sprite.VFlip; - } - - if(lineDotOverflow) { - break; - } - } - - // Rasterize prepared cells in SAT order so the first visible sprite pixel wins. - bool maskActive = false; - bool nonMaskCellEncountered = false; - for(uint16_t i = 0; i < cellCount; i++) { - const GenesisLineSpriteCell& cell = cellList[i]; - if(cell.RawX == 0) { - if(nonMaskCellEncountered || prevLineDotOverflow) { - maskActive = true; - } - continue; - } - - nonMaskCellEncountered = true; - if(maskActive) { - continue; - } - - uint16_t tileIdx = cell.Tile + (uint16_t)(cell.PatternCellOffsetX * cell.VertCells) + cell.PatternCellOffsetY; - uint16_t tileBase = int2 ? (uint16_t)(tileIdx * 64u) : (uint16_t)(tileIdx * 32u); - - for(uint8_t px = 0; px < 8u; px++) { - int16_t screenX = cell.X + (int16_t)(cell.ScreenCellCol * 8u + px); - if(screenX < 0 || screenX >= (int16_t)pixels) { - continue; - } - - uint8_t col = cell.HFlip ? (uint8_t)(7u - px) : px; - uint8_t row = cell.VFlip ? (uint8_t)(cellPixH - 1u - cell.PixRow) : cell.PixRow; - uint8_t pix = FetchTilePixel(tileBase, row, col); - if(pix == 0) { - continue; - } - - if(dst[screenX] != 0) { - _status |= 0x0020u; - continue; - } - - dst[screenX] = (uint8_t)((cell.Priority ? 0x80u : 0x00u) | (cell.Palette << 4) | pix); - } + if(IsH40()) { + return (VBLANK_START_SLOT_H40 - LINE_CHANGE_SLOT_H40) * 16u; } - - _prevLineDotOverflow = lineDotOverflow; + return (VBLANK_START_SLOT_H32 - LINE_CHANGE_SLOT_H32) * 20u; } -void GenesisVdp::Composite(uint16_t line, - const uint8_t* planeB, const uint8_t* planeA, - const uint8_t* spr, uint16_t pixels) +uint32_t GenesisVdp::GetDeferredEventMclk(uint32_t scheduledMclk, uint32_t eventOffsetMclk) const { - if(!_fb) return; - uint32_t* outLine = _fb + (uint32_t)line * _fbW; - - // Priority order (front to back): - // 1. hi-pri window - // 2. hi-pri sprite - // 3. hi-pri plane A - // 4. hi-pri plane B - // 5. lo-pri window - // 6. lo-pri sprite - // 7. lo-pri plane A - // 8. lo-pri plane B - // 9. backdrop - - uint8_t bgIdx = _reg[7] & 0x3Fu; - bool shMode = ShadowHlEnabled(); - - // Shadow/highlight shade codes - enum : uint8_t { Shade_Shadow = 0, Shade_Normal = 1, Shade_Highlight = 2 }; - - for(uint16_t x = 0; x < pixels; x++) { - uint8_t pB = planeB[x]; - uint8_t pA = planeA[x]; // may have bit6 set = window - uint8_t pS = spr[x]; - - // Decode layer flags - bool winSrc = (pA & 0x40u) != 0; - bool winHi = winSrc && ((pA & 0x80u) != 0); - bool winVis = winSrc && ((pA & 0x0Fu) != 0); - bool sprHi = (pS & 0x80u) != 0; - bool sprVis = (pS & 0x0Fu) != 0; - bool pAHi = !winSrc && ((pA & 0x80u) != 0); - bool pAVis = !winSrc && ((pA & 0x0Fu) != 0); - bool pBHi = (pB & 0x80u) != 0; - bool pBVis = (pB & 0x0Fu) != 0; - - // Determine shade in shadow/highlight mode - uint8_t shade = Shade_Normal; - bool sprIsOp = false; // sprite is a shadow/highlight operator - - if(shMode) { - // Step 1: any hi-pri visible plane or window → normal - if((winHi && winVis) || (pAHi && pAVis) || (pBHi && pBVis)) { - shade = Shade_Normal; - } - // Step 2: sprite determines shade (operators are transparent) - if(sprVis) { - uint8_t sprPal = (pS >> 4) & 3u; - uint8_t sprColor = pS & 0x0Fu; - if(!sprHi && sprPal == 3u) { - if(sprColor == 14u) { - // Shadow operator: force shadow, sprite is transparent - shade = Shade_Shadow; - sprIsOp = true; - } else if(sprColor == 15u) { - // Highlight operator: lift one brightness level, sprite is transparent. - // Doc: "If the pixel is shadowed by the first rule, it will appear normal." - // Shadow→Normal, Normal→Highlight. - shade = (shade == Shade_Shadow) ? Shade_Normal : Shade_Highlight; - sprIsOp = true; - } - // else: lo-pri pal-3 regular sprite — shade stays as determined by planes - } else if(sprHi) { - // High-priority non-operator sprite → normal brightness - shade = Shade_Normal; - } else if(sprColor == 15u) { - // Lo-pri sprite with color 15 in non-operator palette → always Normal. - // Doc: "Pixels in sprites using colour 15 of palette lines 1, 2 or 3 - // will always appear normal." - shade = Shade_Normal; - } - } - } - - // Select cramIdx using priority chain. - // In S/H mode, operator sprites are transparent (skip them in color selection). - uint8_t cramIdx = bgIdx; // backdrop fallback - bool backdrop = true; - - if (winHi && winVis) { cramIdx = (uint8_t)(((pA >> 4) & 3u) * 16u + (pA & 0x0Fu)); backdrop = false; } - else if (sprHi && sprVis && !sprIsOp) { cramIdx = (uint8_t)(((pS >> 4) & 3u) * 16u + (pS & 0x0Fu)); backdrop = false; } - else if (pAHi && pAVis) { cramIdx = (uint8_t)(((pA >> 4) & 3u) * 16u + (pA & 0x0Fu)); backdrop = false; } - else if (pBHi && pBVis) { cramIdx = (uint8_t)(((pB >> 4) & 3u) * 16u + (pB & 0x0Fu)); backdrop = false; } - else if (!winHi && winVis) { cramIdx = (uint8_t)(((pA >> 4) & 3u) * 16u + (pA & 0x0Fu)); backdrop = false; } - else if (!sprHi && sprVis && !sprIsOp) { cramIdx = (uint8_t)(((pS >> 4) & 3u) * 16u + (pS & 0x0Fu)); backdrop = false; } - else if (!pAHi && pAVis) { cramIdx = (uint8_t)(((pA >> 4) & 3u) * 16u + (pA & 0x0Fu)); backdrop = false; } - else if (!pBHi && pBVis) { cramIdx = (uint8_t)(((pB >> 4) & 3u) * 16u + (pB & 0x0Fu)); backdrop = false; } - - cramIdx &= 0x3Fu; - _lineBackdropMask[x] = backdrop ? 1u : 0u; - - if(shMode) { - switch(shade) { - case Shade_Shadow: outLine[x] = _shadowPalette [cramIdx]; break; - case Shade_Highlight: outLine[x] = _highlightPalette[cramIdx]; break; - default: outLine[x] = _palette [cramIdx]; break; - } - } else { - outLine[x] = _palette[cramIdx]; - } + if(scheduledMclk != UINT32_MAX) { + return (_mclkPos <= scheduledMclk) ? scheduledMclk : UINT32_MAX; } - // Fill any framebuffer pixels beyond active display with black - uint32_t black = 0xFF000000u; - for(uint32_t x = pixels; x < _fbW; x++) { - outLine[x] = black; - } - for(uint32_t x = pixels; x < std::min(_fbW, 320u); x++) { - _lineBackdropMask[x] = 0u; - } + uint32_t eventMclk = GetFirstVBlankLineMclk() + eventOffsetMclk; + return (_mclkPos <= eventMclk) ? eventMclk : UINT32_MAX; } -uint32_t GenesisVdp::GetVIntEventOffsetMclk() const +uint32_t GenesisVdp::PredictNextHIntMclk() const { - if(IsH40()) { - // Match BlastEm's VINT scheduler for H40 mode. - return MCLKS_PER_LINE - (LINE_CHANGE_SLOT_H40 - VINT_SLOT_H40) * 16u; + uint32_t activeHeight = (uint32_t)ActiveHeight(); + uint32_t currentLine = _mclkPos / MCLKS_PER_LINE; + if(currentLine >= activeHeight) { + return UINT32_MAX; } - // H32 wraps from hslot 147 to 233 before reaching hslot 0. - return (VINT_SLOT_H32 + 256u - 233u + 148u - LINE_CHANGE_SLOT_H32) * 20u; -} - -uint32_t GenesisVdp::GetVBlankFlagOffsetMclk() const -{ - if(IsH40()) { - return (VBLANK_START_SLOT_H40 - LINE_CHANGE_SLOT_H40) * 16u; + // Mirror EndLine() exactly instead of relying on a single-line formula. + // This keeps the scheduler aligned with the live counter/reload rules when + // the current line is near the active->inactive transition. + int counter = _hintCounter; + for(uint32_t line = currentLine; line < activeHeight; line++) { + uint32_t lineEndMclk = (line + 1u) * MCLKS_PER_LINE; + if(counter <= 0) { + return lineEndMclk; + } + counter--; } - return (VBLANK_START_SLOT_H32 - LINE_CHANGE_SLOT_H32) * 20u; + + return UINT32_MAX; } void GenesisVdp::ProcessDeferredInterruptFlags() @@ -2840,66 +2908,14 @@ void GenesisVdp::BeginLine(uint16_t line, uint32_t* fb, uint32_t fbW, uint32_t f _status &= ~0x0008u; if(DispEnabled()) { - // Initialize per-line slot state - _slotCycles = IsH40() ? 16u : 20u; - _maxSpritesLine = IsH40() ? MAX_SPRITES_LINE_H40 : MAX_SPRITES_LINE_H32; - _maxDrawsLine = IsH40() ? MAX_DRAWS_H40 : MAX_DRAWS_H32; - _slotIndex = 0; - _bufAOff = 0; - _bufBOff = 0; - memset(_tmpBufA, 0, sizeof(_tmpBufA)); - memset(_tmpBufB, 0, sizeof(_tmpBufB)); - - // Clear sprite linebuf for this line (_compositebuf is fully - // overwritten by SlotRenderMapOutput — no memset needed). - SlotClearLinebuf(); - - // Load hscroll for this line - _hscrollA = GetHScrollRaw(line, true); - _hscrollB = GetHScrollRaw(line, false); - _hscrollAFine = _hscrollA & 0x0Fu; - _hscrollBFine = _hscrollB & 0x0Fu; - ScrollTraceLog(_frameCount, _scanline, -1, - "HS_BEGIN mode=%u base=%04X hA=%04X hB=%04X fineA=%u fineB=%u", - (unsigned)(_reg[11] & 0x03u), (unsigned)HScrollBase(), - (unsigned)_hscrollA, (unsigned)_hscrollB, - (unsigned)_hscrollAFine, (unsigned)_hscrollBFine); - - // Batch sprite processing: scan + build draw list + render into linebuf. - // Sprites are processed atomically so that column slots can read from - // a fully-populated _linebuf. Per-slot sprite timing is a future step. - _sprInfoCount = 0; - _sprDraws = _maxSpritesLine; - _sprCurSlot = 0; - _sprRenderIdx = 0; - _sprRenderCell = 0; - _sprScanLink = 0; - _sprScanDone = false; - _sprCellBudget = (uint8_t)(ActiveWidth() / 8u); // H40=40, H32=32 - - for(uint16_t s = 0; s < (IsH40() ? 80u : 64u) && !_sprScanDone; s++) { - SlotScanSpriteTable(); - } - // Match BlastEm's mode-5 sprite-X ordering: - // start from the scanned slot count, wrap, then consume entries during - // a full max-sprites-line pass instead of reading 0..count-1 directly. - _sprCurSlot = (int8_t)_sprInfoCount; - for(uint8_t s = 0; s < _maxSpritesLine; s++) { - SlotReadSpriteX(); - } - _sprRenderIdx = 0; - _sprRenderCell = 0; - uint8_t drawCount = (uint8_t)(_maxSpritesLine - _sprDraws); - while(_sprRenderIdx < drawCount && _sprCellBudget > 0) { - SlotSpriteRender(); - } - // Phantom mask sprites: clear _sprCanMask if unused draw slots remain - // (see SlotSpriteRender for the full explanation). - if(_sprRenderIdx >= drawCount && drawCount < _maxSpritesLine) { - _sprCanMask = false; + ResetActiveLineSlotState(line); + if(!_sprPipelinePrimed) { + BootstrapSpritePipeline(line); + ResetActiveLineSlotState(line); } - // BlastEm does not feed sprite overflow into next-line mask activation. _prevLineDotOverflow = false; + } else { + _sprPipelinePrimed = false; } } else if(line == activeH) { // First V-blank line. @@ -2907,9 +2923,11 @@ void GenesisVdp::BeginLine(uint16_t line, uint32_t* fb, uint32_t fbW, uint32_t f // Schedule VINT and the V-blank status flag at their actual in-line offsets. _vintSetMclk = (uint32_t)line * MCLKS_PER_LINE + GetVIntEventOffsetMclk(); _vblankSetMclk = (uint32_t)line * MCLKS_PER_LINE + GetVBlankFlagOffsetMclk(); + _sprPipelinePrimed = false; } else { // Remaining V-blank lines. _status |= 0x0008u; + _sprPipelinePrimed = false; } } @@ -3005,10 +3023,12 @@ void GenesisVdp::BeginFrame(uint32_t* fb, uint32_t fbW, uint32_t fbH) _frameFbW = fbW; _frameFbH = fbH; _mclkPos = 0; + _frameStartMasterClock = _backend ? _backend->GetMasterClock() : 0u; _lineBegun = false; _vintFiredFrame = false; _vintSetMclk = UINT32_MAX; _vblankSetMclk = UINT32_MAX; + _sprPipelinePrimed = false; if(_dmaTraceFile && _frameCount <= 160) { fprintf(_dmaTraceFile, "--- FRAME %u dmaType=%d dmaLen=%u status=%04X ---\n", _frameCount, (int)_dmaType, _dmaLen, _status); @@ -3130,6 +3150,116 @@ bool GenesisVdp::GetDebugTraceLines(GenesisTraceBufferKind kind, vector& return false; } +GenesisTraceConfig GenesisVdp::GetDefaultTraceConfig(GenesisTraceBufferKind kind) +{ + GenesisTraceConfig config = {}; + switch(kind) { + case GenesisTraceBufferKind::Sprite: + config.FrameStart = kSpriteTraceFrameStartDefault; + config.FrameEnd = kSpriteTraceFrameEndDefault; + config.LineStart = kSpriteTraceLineStartDefault; + config.LineEnd = kSpriteTraceLineEndDefault; + config.MaxLines = kSpriteTraceMaxLinesDefault; + break; + + case GenesisTraceBufferKind::Compose: + config.FrameStart = kComposeTraceFrameStartDefault; + config.FrameEnd = kComposeTraceFrameEndDefault; + config.LineStart = kComposeTraceLineStartDefault; + config.LineEnd = kComposeTraceLineEndDefault; + config.XStart = kComposeTraceXStartDefault; + config.XEnd = kComposeTraceXEndDefault; + config.MaxLines = kComposeTraceMaxLinesDefault; + break; + + case GenesisTraceBufferKind::Scroll: + config.FrameStart = kScrollTraceFrameStartDefault; + config.FrameEnd = kScrollTraceFrameEndDefault; + config.LineStart = kScrollTraceLineStartDefault; + config.LineEnd = kScrollTraceLineEndDefault; + config.ColumnStart = kScrollTraceColumnStartDefault; + config.ColumnEnd = kScrollTraceColumnEndDefault; + config.MaxLines = kScrollTraceMaxLinesDefault; + break; + + case GenesisTraceBufferKind::HScrollDma: + config.FrameStart = kHScrollDmaTraceFrameStartDefault; + config.FrameEnd = kHScrollDmaTraceFrameEndDefault; + config.DstStart = kHScrollDmaTraceDstStartDefault; + config.DstEnd = kHScrollDmaTraceDstEndDefault; + config.MaxLines = kHScrollDmaTraceMaxLinesDefault; + break; + + case GenesisTraceBufferKind::Dma: + default: + break; + } + return config; +} + +bool GenesisVdp::ConfigureTrace(GenesisTraceBufferKind kind, const GenesisTraceConfig& config) +{ + switch(kind) { + case GenesisTraceBufferKind::Sprite: + kSpriteTraceFrameStart = config.FrameStart; + kSpriteTraceFrameEnd = config.FrameEnd; + kSpriteTraceLineStart = config.LineStart; + kSpriteTraceLineEnd = config.LineEnd; + kSpriteTraceMaxLines = std::max(1u, config.MaxLines); + if(kSpriteTraceFrameStart > kSpriteTraceFrameEnd) std::swap(kSpriteTraceFrameStart, kSpriteTraceFrameEnd); + if(kSpriteTraceLineStart > kSpriteTraceLineEnd) std::swap(kSpriteTraceLineStart, kSpriteTraceLineEnd); + sSpriteTraceLines = 0; + sSpriteTraceBuffer.clear(); + return true; + + case GenesisTraceBufferKind::Compose: + kComposeTraceFrameStart = config.FrameStart; + kComposeTraceFrameEnd = config.FrameEnd; + kComposeTraceLineStart = config.LineStart; + kComposeTraceLineEnd = config.LineEnd; + kComposeTraceXStart = config.XStart; + kComposeTraceXEnd = config.XEnd; + kComposeTraceMaxLines = std::max(1u, config.MaxLines); + if(kComposeTraceFrameStart > kComposeTraceFrameEnd) std::swap(kComposeTraceFrameStart, kComposeTraceFrameEnd); + if(kComposeTraceLineStart > kComposeTraceLineEnd) std::swap(kComposeTraceLineStart, kComposeTraceLineEnd); + if(kComposeTraceXStart > kComposeTraceXEnd) std::swap(kComposeTraceXStart, kComposeTraceXEnd); + sComposeTraceLines = 0; + sComposeTraceBuffer.clear(); + return true; + + case GenesisTraceBufferKind::Scroll: + kScrollTraceFrameStart = config.FrameStart; + kScrollTraceFrameEnd = config.FrameEnd; + kScrollTraceLineStart = config.LineStart; + kScrollTraceLineEnd = config.LineEnd; + kScrollTraceColumnStart = config.ColumnStart; + kScrollTraceColumnEnd = config.ColumnEnd; + kScrollTraceMaxLines = std::max(1u, config.MaxLines); + if(kScrollTraceFrameStart > kScrollTraceFrameEnd) std::swap(kScrollTraceFrameStart, kScrollTraceFrameEnd); + if(kScrollTraceLineStart > kScrollTraceLineEnd) std::swap(kScrollTraceLineStart, kScrollTraceLineEnd); + if(kScrollTraceColumnStart > kScrollTraceColumnEnd) std::swap(kScrollTraceColumnStart, kScrollTraceColumnEnd); + sScrollTraceLines = 0; + sScrollTraceBuffer.clear(); + return true; + + case GenesisTraceBufferKind::HScrollDma: + kHScrollDmaTraceFrameStart = config.FrameStart; + kHScrollDmaTraceFrameEnd = config.FrameEnd; + kHScrollDmaTraceDstStart = config.DstStart; + kHScrollDmaTraceDstEnd = config.DstEnd; + kHScrollDmaTraceMaxLines = std::max(1u, config.MaxLines); + if(kHScrollDmaTraceFrameStart > kHScrollDmaTraceFrameEnd) std::swap(kHScrollDmaTraceFrameStart, kHScrollDmaTraceFrameEnd); + if(kHScrollDmaTraceDstStart > kHScrollDmaTraceDstEnd) std::swap(kHScrollDmaTraceDstStart, kHScrollDmaTraceDstEnd); + sHScrollDmaTraceLines = 0; + sHScrollDmaTraceBuffer.clear(); + return true; + + case GenesisTraceBufferKind::Dma: + default: + return false; + } +} + void GenesisVdp::AdvanceToMclk(uint32_t targetMclk) { while(_mclkPos < targetMclk) { @@ -3151,30 +3281,16 @@ void GenesisVdp::AdvanceToMclk(uint32_t targetMclk) while(_mclkPos < targetMclk && _slotIndex < slotCount) { const SlotDescriptor& slot = table[_slotIndex]; - - // Direct dispatch — inlined to avoid double-switch through DispatchSlot. - // Sprite/clear ops are already batched in BeginLine. - switch(slot.op) { - case SlotOp::ReadMapScrollA: SlotReadMapScrollA(slot.column); break; - case SlotOp::ReadMapScrollB: SlotReadMapScrollB(slot.column); break; - case SlotOp::RenderMap1: SlotRenderMap1(); break; - case SlotOp::RenderMap2: SlotRenderMap2(); break; - case SlotOp::RenderMap3: SlotRenderMap3(); break; - case SlotOp::RenderMapOutput: SlotRenderMapOutput(slot.column); break; - case SlotOp::HScrollLoad: SlotHScrollLoad(); break; - case SlotOp::ExternalSlot: SlotExternalSlot(); break; - default: break; // SpriteRender, ReadSpriteX, ClearLinebuf, Refresh, Nop - } + DispatchSlot(slot); _mclkPos += _slotCycles; _slotIndex++; } - // Tail column render (C40 in H40, C32 in H32) to complete the visible width. - // The static slot table stops at C38/C30, so emit one final map/composite step - // once per line before flushing. - if(_slotIndex == slotCount) { - int16_t tailCol = IsH40() ? 40 : 32; + // Tail column render to complete the visible width. H40 still needs the + // synthetic C40 step; H32 now carries the C32 block in the slot table. + if(IsH40() && _slotIndex == slotCount) { + int16_t tailCol = 40; SlotReadMapScrollA(tailCol); SlotRenderMap1(); SlotRenderMap2(); @@ -3204,6 +3320,7 @@ void GenesisVdp::AdvanceToMclk(uint32_t targetMclk) } else { // --- V-blank or display-disabled: skip to line end --- if(lineEnd <= targetMclk) { + SyncPortAccessState(_frameStartMasterClock + lineEnd); // Display disabled during active region — fill with backdrop if(currentLine < (uint32_t)ActiveHeight() && !DispEnabled()) { if(_fb && currentLine < _fbH) { @@ -3218,6 +3335,7 @@ void GenesisVdp::AdvanceToMclk(uint32_t targetMclk) _lineBegun = false; _mclkPos = lineEnd; } else { + SyncPortAccessState(_frameStartMasterClock + targetMclk); _mclkPos = targetMclk; break; } @@ -3242,42 +3360,59 @@ void GenesisVdp::AdvanceToMclk(uint32_t targetMclk) uint32_t GenesisVdp::NextVIntMclk() const { if(!VIntEnabled() || _vintFiredFrame) return UINT32_MAX; - if(_vintSetMclk != UINT32_MAX) { - return (_mclkPos <= _vintSetMclk) ? _vintSetMclk : UINT32_MAX; - } - - uint32_t vintMclk = (uint32_t)ActiveHeight() * MCLKS_PER_LINE + GetVIntEventOffsetMclk(); - return (_mclkPos <= vintMclk) ? vintMclk : UINT32_MAX; + return GetDeferredEventMclk(_vintSetMclk, GetVIntEventOffsetMclk()); } uint32_t GenesisVdp::NextVBlankFlagMclk() const { if((_status & 0x0008u) != 0) return UINT32_MAX; - if(_vblankSetMclk != UINT32_MAX) { - return (_mclkPos <= _vblankSetMclk) ? _vblankSetMclk : UINT32_MAX; - } - - uint32_t vblankMclk = (uint32_t)ActiveHeight() * MCLKS_PER_LINE + GetVBlankFlagOffsetMclk(); - return (_mclkPos <= vblankMclk) ? vblankMclk : UINT32_MAX; + return GetDeferredEventMclk(_vblankSetMclk, GetVBlankFlagOffsetMclk()); } -uint32_t GenesisVdp::NextHIntMclk() const +uint32_t GenesisVdp::Next68kBusDmaReleaseMclk() const { - if(!HIntEnabled()) return UINT32_MAX; + if(!Is68kBusDmaActive() || !DispEnabled()) { + return UINT32_MAX; + } + uint32_t currentLine = _mclkPos / MCLKS_PER_LINE; + if(currentLine >= (uint32_t)ActiveHeight()) { + return UINT32_MAX; + } + + const SlotDescriptor* table = IsH40() ? kSlotTableH40 : kSlotTableH32; + uint16_t slotCount = IsH40() ? SLOT_COUNT_H40 : SLOT_COUNT_H32; + uint32_t slotCycles = IsH40() ? 16u : 20u; + uint64_t currentClock = GetCurrentVdpClock(); + uint64_t readyClock = std::max(_dmaBusReadyClock, currentClock); + uint32_t wordsRemaining = _dmaLen; + + for(uint32_t line = currentLine; line < (uint32_t)ActiveHeight(); line++) { + uint16_t startIndex = (line == currentLine && _lineBegun) ? _slotIndex : 0u; + uint32_t lineBaseMclk = line * MCLKS_PER_LINE; + for(uint16_t i = startIndex; i < slotCount; i++) { + if(table[i].op != SlotOp::ExternalSlot) { + continue; + } - // H-int only fires during active display lines (0 .. ActiveHeight()-1). - // During V-blank the counter is reloaded each line but no interrupt is raised. - if(currentLine >= (uint32_t)ActiveHeight()) return UINT32_MAX; + uint64_t slotClock = _frameStartMasterClock + lineBaseMclk + (uint64_t)i * slotCycles; + if(slotClock < readyClock) { + continue; + } - uint32_t eventMclk = (currentLine + (uint32_t)_hintCounter + 1u) * MCLKS_PER_LINE; - uint32_t vblankMclk = (uint32_t)ActiveHeight() * MCLKS_PER_LINE; + if(--wordsRemaining == 0u) { + return lineBaseMclk + std::min((uint32_t)(i + 1u) * slotCycles, MCLKS_PER_LINE); + } + } + } - // If the counter roll-over would land in or past V-blank, the interrupt will - // not fire this frame (it fires at REG_HINT lines from the next frame start). - if(eventMclk > vblankMclk) return UINT32_MAX; + return UINT32_MAX; +} - return eventMclk; +uint32_t GenesisVdp::NextHIntMclk() const +{ + if(!HIntEnabled()) return UINT32_MAX; + return PredictNextHIntMclk(); } // =========================================================================== @@ -3286,6 +3421,8 @@ uint32_t GenesisVdp::NextHIntMclk() const void GenesisVdp::SaveState(vector& out) const { + uint64_t currentClock = GetCurrentVdpClock(); + AppV(out, _vram); AppV(out, _cram); AppV(out, _vsram); @@ -3342,6 +3479,39 @@ void GenesisVdp::SaveState(vector& out) const AppV(out, _vintSetMclk); // Deferred HINT delivery state (added in version 35) AppV(out, _hintNew); + // Timed FIFO/read pipeline state (added in version 36) + AppV(out, _readPending); + uint32_t readReadyDelta = (_readPending && _readReadyClock > currentClock) + ? (uint32_t)(_readReadyClock - currentClock) + : 0u; + AppV(out, readReadyDelta); + for(int i = 0; i < 4; i++) { + uint32_t fifoReadyDelta = (_fifo[i].readyClock > currentClock) + ? (uint32_t)(_fifo[i].readyClock - currentClock) + : 0u; + AppV(out, fifoReadyDelta); + } + // Bus68k DMA source-ready timing (added in version 37) + uint32_t dmaBusReadyDelta = (_dmaBusReadyClock > currentClock) + ? (uint32_t)(_dmaBusReadyClock - currentClock) + : 0u; + AppV(out, dmaBusReadyDelta); + // Slot-driven sprite pipeline state (added in version 38) + AppV(out, _sprSetupTargetLine); + AppV(out, _sprScanTargetLine); + AppV(out, _sprPipelinePrimed); + AppV(out, _sprInfoCount); + AppV(out, _sprDraws); + AppV(out, _sprCurSlot); + AppV(out, _sprRenderIdx); + AppV(out, _sprRenderCell); + AppV(out, _sprScanLink); + AppV(out, _sprScanDone); + AppV(out, _sprMasked); + AppV(out, _sprRenderFullSpan); + AppV(out, _sprCellBudget); + AppV(out, _spriteInfoList); + AppV(out, _spriteDrawList); } bool GenesisVdp::LoadState(const vector& data, size_t& offset) @@ -3437,11 +3607,93 @@ bool GenesisVdp::LoadState(const vector& data, size_t& offset) _hintNew = false; } + _frameStartMasterClock = _backend ? (_backend->GetMasterClock() - _mclkPos) : 0u; + + // Timed FIFO/read pipeline state (version 36+) + uint64_t currentClock = GetCurrentVdpClock(); + if(offset + sizeof(_readPending) + sizeof(uint32_t) + 4u * sizeof(uint32_t) <= data.size()) { + uint32_t readReadyDelta = 0; + if(!RdV(data, offset, _readPending)) return false; + if(!RdV(data, offset, readReadyDelta)) return false; + _readReadyClock = currentClock + readReadyDelta; + for(int i = 0; i < 4; i++) { + uint32_t fifoReadyDelta = 0; + if(!RdV(data, offset, fifoReadyDelta)) return false; + _fifo[i].readyClock = currentClock + fifoReadyDelta; + } + } else { + _readPending = false; + _readReadyClock = currentClock; + for(int i = 0; i < 4; i++) { + _fifo[i].readyClock = currentClock; + } + } + + // Bus68k DMA source-ready timing (version 37+) + if(offset + sizeof(uint32_t) <= data.size()) { + uint32_t dmaBusReadyDelta = 0; + if(!RdV(data, offset, dmaBusReadyDelta)) return false; + _dmaBusReadyClock = currentClock + dmaBusReadyDelta; + } else { + _dmaBusReadyClock = currentClock + _dmaBusStartDelayMclk; + } + + // Slot-driven sprite pipeline state (version 38+) + if(offset + + sizeof(_sprSetupTargetLine) + + sizeof(_sprScanTargetLine) + + sizeof(_sprPipelinePrimed) + + sizeof(_sprInfoCount) + + sizeof(_sprDraws) + + sizeof(_sprCurSlot) + + sizeof(_sprRenderIdx) + + sizeof(_sprRenderCell) + + sizeof(_sprScanLink) + + sizeof(_sprScanDone) + + sizeof(_sprMasked) + + sizeof(_sprRenderFullSpan) + + sizeof(_sprCellBudget) + + sizeof(_spriteInfoList) + + sizeof(_spriteDrawList) <= data.size()) { + if(!RdV(data, offset, _sprSetupTargetLine)) return false; + if(!RdV(data, offset, _sprScanTargetLine)) return false; + if(!RdV(data, offset, _sprPipelinePrimed)) return false; + if(!RdV(data, offset, _sprInfoCount)) return false; + if(!RdV(data, offset, _sprDraws)) return false; + if(!RdV(data, offset, _sprCurSlot)) return false; + if(!RdV(data, offset, _sprRenderIdx)) return false; + if(!RdV(data, offset, _sprRenderCell)) return false; + if(!RdV(data, offset, _sprScanLink)) return false; + if(!RdV(data, offset, _sprScanDone)) return false; + if(!RdV(data, offset, _sprMasked)) return false; + if(!RdV(data, offset, _sprRenderFullSpan)) return false; + if(!RdV(data, offset, _sprCellBudget)) return false; + if(!RdV(data, offset, _spriteInfoList)) return false; + if(!RdV(data, offset, _spriteDrawList)) return false; + } else { + _sprSetupTargetLine = WrapScanline((uint32_t)_scanline + 1u); + _sprScanTargetLine = WrapScanline((uint32_t)_scanline + 2u); + _sprPipelinePrimed = false; + _sprInfoCount = 0; + _sprDraws = 0; + _sprCurSlot = 0; + _sprRenderIdx = 0; + _sprRenderCell = 0; + _sprScanLink = 0; + _sprScanDone = false; + _sprMasked = false; + _sprRenderFullSpan = false; + _sprCellBudget = (uint8_t)(ActiveWidth() / 8u); + memset(_spriteInfoList, 0, sizeof(_spriteInfoList)); + memset(_spriteDrawList, 0, sizeof(_spriteDrawList)); + } + // Debug register is not serialized; clear to power-on default on load. _debugReg = 0; // Rebuild expanded palette for(uint8_t i = 0; i < 64; i++) RefreshPalette(i); + FifoUpdateStatus(); return true; } diff --git a/Core/Genesis/GenesisVdp.h b/Core/Genesis/GenesisVdp.h index 9de2307..400622c 100644 --- a/Core/Genesis/GenesisVdp.h +++ b/Core/Genesis/GenesisVdp.h @@ -67,6 +67,8 @@ class GenesisVdp // Read-ahead buffer — primed whenever a read address/code is set uint16_t _readBuf = 0; + bool _readPending = false; + uint64_t _readReadyClock = 0; // Byte-write accumulator — holds the high byte of a pending word write // to the data or control port. Separate from _readBuf to avoid aliasing. @@ -113,6 +115,7 @@ class GenesisVdp uint16_t _dmaFillVal = 0; // fill word (written via data port) bool _dmaFillPend = false; // fill word received, waiting to execute uint8_t _dmaBusStartDelayMclk = 0; // startup latency before first 68K DMA word + uint64_t _dmaBusReadyClock = 0; // absolute time when Bus68k DMA may source its first word uint8_t _dmaBusMclkRemainder = 0; // leftover master clocks for 68K DMA pacing uint8_t _dmaVdpMclkRemainder = 0; // leftover master clocks for internal VDP DMA pacing @@ -125,6 +128,7 @@ class GenesisVdp uint16_t addr = 0; // latched _addrReg at write time uint8_t code = 0; // latched _codeReg at write time (CD3:CD0) uint8_t pad = 0; + uint64_t readyClock = 0; }; FifoEntry _fifo[4] = {}; uint8_t _fifoRead = 0; // ring read index @@ -157,6 +161,7 @@ class GenesisVdp // Master-clock timeline (event-driven scheduler) // ----------------------------------------------------------------------- uint32_t _mclkPos = 0; // master-clock position within current frame + uint64_t _frameStartMasterClock = 0; bool _lineBegun = false; // BeginLine called for _mclkPos/MCLKS_PER_LINE bool _vintFiredFrame = false; // V-int already fired this frame uint32_t _vintSetMclk = UINT32_MAX; // mclk at which V-int pending should go active @@ -180,6 +185,7 @@ class GenesisVdp ReadSpriteX, // read sprite X position / build draw list RenderMap3, // render col_1 of plane B into _tmpBufB RenderMapOutput, // render col_2 of B, composite 16px, advance bufs + SpriteRenderOnly, // render_sprite_cells without advancing SAT scan SpriteRender, // render_sprite_cells + scan_sprite_table HScrollLoad, // load hscroll_a/b from HSCROLL table Refresh, // DRAM refresh (no external slot / no DMA src) @@ -239,7 +245,7 @@ class GenesisVdp struct SpriteInfo { uint16_t index = 0; // SAT entry index - int16_t y = 0; // screen Y position + uint16_t y = 0; // raw SAT Y position (9/10-bit counter space) uint8_t size = 0; // SAT raw size nibble: HHVV, each field encoded as (cells-1) }; @@ -264,21 +270,27 @@ class GenesisVdp uint8_t _sprRenderIdx = 0; // current draw entry for SpriteRender uint8_t _sprRenderCell = 0; // current cell within sprite being rendered uint8_t _sprScanLink = 0; // current SAT link for scan_sprite_table + uint16_t _sprSetupTargetLine = 0; // line currently being built by ReadSpriteX/SpriteRender + uint16_t _sprScanTargetLine = 0; // future line currently being scanned into _spriteInfoList bool _sprScanDone = false; bool _sprCanMask = false; // non-zero X sprite seen (carries across lines, NOT reset per line) bool _sprMasked = false; // X=0 mask active + bool _sprRenderFullSpan = false; // ClearLinebuf late phase walks all sprite slots, including stale phantom slots + bool _sprPipelinePrimed = false; uint8_t _maxSpritesLine = MAX_SPRITES_LINE_H40; uint8_t _maxDrawsLine = MAX_DRAWS_H40; uint8_t _sprCellBudget = 40; // max sprite cells processed this line (H40=40, H32=32) + uint16_t _currentHSlot = 0xFFFFu; // currently dispatched slot hcounter (trace attribution only) - // SAT cache: snapshot of the sprite attribute table taken at EndLine(). - // BeginLine()'s batch sprite scan reads from this cache so that the scan - // sees the SAT state from the *previous* line (before the 68K ran the - // current line's cycles), matching hardware timing for mid-frame SAT - // rewrites used by sprite-multiplexing games. + // SAT cache used by the slot-driven sprite scan. It is seeded from VRAM and + // mirrored by VRAM writes so SAT updates are visible at BlastEm-compatible + // timing without making the scan read unrelated live VRAM state. static constexpr uint16_t SAT_CACHE_SIZE = 80u * 8u; // 80 entries × 8 bytes uint8_t _satCache[SAT_CACHE_SIZE] = {}; void CacheSpriteTable(); + void UpdateSatCacheByte(uint16_t byteAddr, uint8_t value); + void WriteVramByte(uint16_t byteAddr, uint8_t value); + void WriteVramWord(uint16_t byteAddr, uint16_t value); // --- Slot operation methods --- void DispatchSlot(const SlotDescriptor& slot); @@ -294,9 +306,19 @@ class GenesisVdp void SlotExternalSlot(); void SlotClearLinebuf(); void SlotHScrollLoad(); + void ResetActiveLineSlotState(uint16_t line); + void BeginNextLineSpriteSetup(uint16_t line); + void LoadLineHScroll(uint16_t line); + uint16_t WrapScanline(uint32_t line) const; + void PrimeSpriteScanForLine(uint16_t line); + void PrimeSpriteDrawListForLine(uint16_t line); + void PrimeSpriteLinebufFromDrawList(); + void BootstrapSpritePipeline(uint16_t line); + uint16_t GetSpriteSetupTraceLine() const; + void GetSpriteRenderTracePosition(uint32_t& frame, uint16_t& line) const; void FifoDrainOne(); // drain one FIFO entry (execute the write) void FifoUpdateStatus(); // update status bits 9:8 from _fifoCount - void RunDmaSrc(); // feed one Bus68k DMA word into FIFO (called at external slots) + void RunDmaSrc(uint64_t accessClock); // feed one Bus68k DMA word into FIFO void FlushCompositeBuf(uint16_t line); // ----------------------------------------------------------------------- @@ -322,9 +344,17 @@ class GenesisVdp // Control port helpers void AdvanceAddr(); void PrimeBuf(); + void SyncPortAccessState(uint64_t accessClock); + void StartReadPipeline(uint64_t accessClock); void HandleControlWrite(uint16_t word); void WriteReg(uint8_t r, uint8_t val); void BeginOperation(); // called after address/code are finalized + uint64_t GetCurrentVdpClock() const; + uint64_t GetScheduledAccessClock() const; + uint8_t GetEffectiveFifoCount(uint64_t accessClock) const; + uint64_t GetNextReadyFifoClock(uint64_t accessClock) const; + uint32_t GetPortLatencyMclk() const; + bool IsBlankingAtClock(uint64_t accessClock) const; // DMA void ExecDma(); @@ -333,29 +363,13 @@ class GenesisVdp void ExecDmaCopy(uint32_t maxWords); void AdvanceDmaAddr(); void StartDmaIfArmed(); + uint16_t ReadDmaBusWord(uint32_t srcByteAddr) const; // ----------------------------------------------------------------------- // Rendering // ----------------------------------------------------------------------- - void RenderScanline(uint16_t line); void RenderBackdrop(uint16_t line); - // Per-pixel encoding: - // bit 7 : priority flag - // bits 6:4: palette (0-3) — note only 2 bits needed, stored in 4:3 - // bits 3:0: color index within palette (0 = transparent for planes/sprites) - // Full CRAM index = ((pix >> 4) & 0xF) * 16 + (pix & 0xF) - // but palette is 2 bits (bits 5:4) and color index 4 bits (3:0), so: - // CRAM index = ((pix >> 4) & 3) * 16 + (pix & 0xF) - - void RenderPlaneB (uint16_t line, uint8_t* dst, uint16_t pixels) const; - void RenderPlaneA (uint16_t line, uint8_t* dst, uint16_t pixels) const; - void RenderWindow (uint16_t line, uint8_t* dst, uint16_t pixels) const; - void RenderSprites(uint16_t line, uint8_t* dst, uint16_t pixels); - void Composite (uint16_t line, - const uint8_t* planeB, const uint8_t* planeA, - const uint8_t* spr, uint16_t pixels); - // Render one 8-wide name-table row into dst[0..7] // Returns true if any pixel in this column is covered by the window plane. void RenderNameTableCol(uint16_t nameWord, uint8_t tileRow, @@ -384,6 +398,7 @@ class GenesisVdp bool VIntEnabled() const { return (_reg[1] & 0x20) != 0; } bool HIntEnabled() const { return (_reg[0] & 0x10) != 0; } bool DmaEnabled() const { return (_reg[1] & 0x10) != 0; } + bool Mode5Enabled() const { return (_reg[1] & 0x04) != 0; } bool ShadowHlEnabled() const { return (_reg[12] & 0x08) != 0; } // Interlace mode from R12 bits [2:1] (LS1-LS0): // 00 = no interlace, 01 = interlace normal, 10 = no interlace, 11 = interlace double. @@ -402,8 +417,11 @@ class GenesisVdp uint16_t HScrollBase() const { return (uint16_t)(_reg[13] & 0x3F) << 10; } uint8_t AutoInc() const { return _reg[15]; } uint8_t HIntReload() const { return _reg[10]; } + uint32_t GetFirstVBlankLineMclk() const; uint32_t GetVIntEventOffsetMclk() const; uint32_t GetVBlankFlagOffsetMclk() const; + uint32_t GetDeferredEventMclk(uint32_t scheduledMclk, uint32_t eventOffsetMclk) const; + uint32_t PredictNextHIntMclk() const; void ProcessDeferredInterruptFlags(); public: @@ -419,6 +437,7 @@ class GenesisVdp uint32_t NextVIntMclk() const; uint32_t NextHIntMclk() const; uint32_t NextVBlankFlagMclk() const; + uint32_t Next68kBusDmaReleaseMclk() const; uint32_t GetMclkPos() const { return _mclkPos; } uint16_t ActiveWidth() const { return (_reg[12] & 0x01) ? 320u : 256u; } @@ -429,6 +448,7 @@ class GenesisVdp // addr is the raw 24-bit bus address (in $C00000-$C0001F range) uint8_t ReadByte (uint32_t addr); void WriteByte(uint32_t addr, uint8_t val); + uint8_t GetPortWaitStates(uint32_t addr, bool isWrite) const; // Called once per scanline from the backend's RunFrame loop. // Renders line `line` into `fb` (ARGB8888, stride `fbW`). @@ -479,6 +499,8 @@ class GenesisVdp bool IsDmaFillPending() const { return _dmaFillPend; } bool IsVIntPending() const { return _vintPending; } bool IsHIntPending() const { return _hintPending; } + static GenesisTraceConfig GetDefaultTraceConfig(GenesisTraceBufferKind kind); + static bool ConfigureTrace(GenesisTraceBufferKind kind, const GenesisTraceConfig& config); // Save / load state helpers (called by GenesisNativeBackend::SaveState/LoadState) void SaveState(vector& out) const; diff --git a/Core/Genesis/IGenesisCoreBackend.h b/Core/Genesis/IGenesisCoreBackend.h deleted file mode 100644 index b243841..0000000 --- a/Core/Genesis/IGenesisCoreBackend.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "pch.h" -#include "Genesis/GenesisTypes.h" -#include "Shared/MemoryType.h" -#include "Shared/SettingTypes.h" - -class IGenesisCoreBackend -{ -public: - virtual ~IGenesisCoreBackend() = default; - - virtual GenesisCoreType GetCoreType() const = 0; - - virtual bool LoadRom(const vector& romData, const char* region, - const uint8_t* saveRamData, uint32_t saveRamSize, - const uint8_t* saveEepromData, uint32_t saveEepromSize) = 0; - - virtual void RunFrame() = 0; - virtual void SyncSaveData() = 0; - - virtual const uint8_t* GetMemoryPointer(MemoryType type, uint32_t& size) = 0; - virtual const uint8_t* GetSaveEeprom(uint32_t& size) = 0; - - virtual bool IsPAL() const = 0; - virtual double GetFps() const = 0; - virtual uint64_t GetMasterClock() const = 0; - virtual uint32_t GetMasterClockRate() const = 0; - - virtual void GetCpuState(GenesisCpuState& state) const = 0; - virtual void GetVdpState(GenesisVdpState& state) const = 0; - virtual void GetVdpRegisters(uint8_t regs[24]) const = 0; - virtual bool GetVdpDebugState(GenesisVdpDebugState& state) const { state = {}; return false; } - virtual bool GetVdpTraceLines(GenesisTraceBufferKind kind, vector& lines) const { lines.clear(); return false; } - virtual void GetFrameSize(uint32_t& width, uint32_t& height) const = 0; - virtual bool GetBackendDebugState(GenesisBackendState& state) const { state = {}; return false; } - - virtual uint8_t ReadMemory(MemoryType type, uint32_t address) = 0; - virtual void WriteMemory(MemoryType type, uint32_t address, uint8_t value) = 0; - - virtual bool SetProgramCounter(uint32_t address) = 0; - virtual uint32_t GetInstructionSize(uint32_t address) = 0; - virtual const char* DisassembleInstruction(uint32_t address) = 0; - - virtual bool SaveState(vector& outState) = 0; - virtual bool LoadState(const vector& state) = 0; -}; diff --git a/Core/Genesis/RefreshParityNote.md b/Core/Genesis/RefreshParityNote.md new file mode 100644 index 0000000..4bbe1ae --- /dev/null +++ b/Core/Genesis/RefreshParityNote.md @@ -0,0 +1,85 @@ +# Genesis Refresh-Delay Parity Note + +## Scope + +This note covers the refresh-delay slice that was added after the open-bus work. + +The goal was not full BlastEm-equivalent prefetch timing. The goal was to stop ignoring refresh entirely and add a backend-level refresh penalty model that matches BlastEm's broad distinction between: + +- normal 68K accesses that can incur refresh delay +- VDP / I/O-style accesses that should be treated as refresh-free for the current access + +## What Changed + +The implementation lives in: + +- [GenesisNativeBackend.h](/d:/Mesen2-Expanded/Core/Genesis/GenesisNativeBackend.h) +- [GenesisNativeBackend.cpp](/d:/Mesen2-Expanded/Core/Genesis/GenesisNativeBackend.cpp) + +Main additions: + +- `_refreshCounterMclk` +- `_refreshLastAccessClock` +- `IsRefreshFreeAccess(...)` +- `GetRefreshWaitStates(...)` + +The backend now: + +1. Tracks refresh timing against the live 68K execution clock using `GetCurrentExecutionMasterClock()`. +2. Accumulates elapsed master clocks into `_refreshCounterMclk`. +3. Applies a periodic wait-state penalty on non-free accesses. +4. Treats these regions as refresh-free for the current access: + - I/O registers + - memory-mode register + - Z80 bus request / reset registers + - mapper registers + - TMSS registers + - VDP ports +5. Serializes the refresh accumulator state in savestates. + +## Current Model + +The current approximation is: + +- refresh interval: `7 * 128` master clocks +- refresh delay: `2` extra 68K cycles per elapsed interval + +That penalty is added through [`CpuBusWaitStates()`](/d:/Mesen2-Expanded/Core/Genesis/GenesisNativeBackend.cpp), not through a deeper instruction scheduler. + +## Why This Was Added + +Before this change, the Genesis native backend had no refresh-delay model at all. + +That left one of the remaining machine-layer gaps identified during the BlastEm comparison: + +- BlastEm advances a refresh counter continuously +- BlastEm suppresses refresh delay on certain free-access paths +- our backend previously did neither + +This note documents the first corrective step, not a full parity endpoint. + +## Limitations + +This is still an approximation. + +It does **not** currently do the following: + +- emulate true prefetch-driven open-bus/refresh interaction +- integrate refresh timing into a fully instruction-exact machine scheduler +- model every special-case access path the same way BlastEm does + +So the correct interpretation is: + +- refresh timing is now represented +- refresh timing is still coarser than BlastEm + +## Expected Next Step + +Refresh timing was the last major non-sprite item from the planned parity list. + +The next meaningful step is either: + +1. targeted verification of the accumulated timing work, or +2. the deferred sprite slot-accuracy rewrite + +The recommended order is verification first, because sprite timing is the most invasive remaining change. diff --git a/Core/Shared/EmuSettings.cpp b/Core/Shared/EmuSettings.cpp index f7849ea..2617e1f 100644 --- a/Core/Shared/EmuSettings.cpp +++ b/Core/Shared/EmuSettings.cpp @@ -132,7 +132,6 @@ void EmuSettings::Serialize(Serializer& s) SV(_genesis.Port1.Type); SV(_genesis.Port2.Type); SV(_genesis.Region); - SV(_genesis.CoreType); break; default: diff --git a/Core/Shared/Emulator.cpp b/Core/Shared/Emulator.cpp index 8aa2424..e2a0878 100644 --- a/Core/Shared/Emulator.cpp +++ b/Core/Shared/Emulator.cpp @@ -360,11 +360,11 @@ void Emulator::PowerCycle() ReloadRom(true); } -bool Emulator::LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle) +bool Emulator::LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle, optional forcedConsoleType) { bool result = false; try { - result = InternalLoadRom(romFile, patchFile, stopRom, forPowerCycle); + result = InternalLoadRom(romFile, patchFile, stopRom, forPowerCycle, forcedConsoleType); } catch(std::exception& ex) { _videoDecoder->StartThread(); _videoRenderer->StartThread(); @@ -380,7 +380,7 @@ bool Emulator::LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, return result; } -bool Emulator::InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle) +bool Emulator::InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle, optional forcedConsoleType) { if(!romFile.IsValid()) { MessageManager::DisplayMessage("Error", "CouldNotLoadFile", romFile.GetFileName()); @@ -436,9 +436,13 @@ bool Emulator::InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool unique_ptr console; LoadRomResult result = LoadRomResult::UnknownType; - //Try loading the rom, give priority to file extension, then trying to check for file signatures if extension is unknown - TryLoadRom(romFile, result, console, false); - TryLoadRom(romFile, result, console, true); + if(forcedConsoleType.has_value()) { + TryLoadRom(romFile, result, console, forcedConsoleType.value()); + } else { + //Try loading the rom, give priority to file extension, then trying to check for file signatures if extension is unknown + TryLoadRom(romFile, result, console, false); + TryLoadRom(romFile, result, console, true); + } if(result != LoadRomResult::Success) { MessageManager::DisplayMessage("Error", "CouldNotLoadFile", romFile.GetFileName()); @@ -572,13 +576,27 @@ void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_pt TryLoadRom(romFile, result, console, useFileSignature); } +void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, ConsoleType consoleType) +{ + switch(consoleType) { + case ConsoleType::Nes: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Snes: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Gameboy: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::PcEngine: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Sms: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Gba: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Ws: TryLoadRom(romFile, result, console, false, true); break; + case ConsoleType::Genesis: TryLoadRom(romFile, result, console, false, true); break; + } +} + template -void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, bool useFileSignature) +void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, bool useFileSignature, bool forceConsoleType) { if(result == LoadRomResult::UnknownType) { string romExt = romFile.GetFileExtension(); vector extensions = T::GetSupportedExtensions(); - if(std::find(extensions.begin(), extensions.end(), romExt) != extensions.end() || (useFileSignature && romFile.CheckFileSignature(T::GetSupportedSignatures()))) { + if(forceConsoleType || std::find(extensions.begin(), extensions.end(), romExt) != extensions.end() || (useFileSignature && romFile.CheckFileSignature(T::GetSupportedSignatures()))) { //Keep a copy of the current state of _consoleMemory ConsoleMemoryInfo consoleMemory[DebugUtilities::GetMemoryTypeCount()] = {}; memcpy(consoleMemory, _consoleMemory, sizeof(_consoleMemory)); diff --git a/Core/Shared/Emulator.h b/Core/Shared/Emulator.h index 0076ad9..6e0f8c6 100644 --- a/Core/Shared/Emulator.h +++ b/Core/Shared/Emulator.h @@ -132,11 +132,12 @@ class Emulator double GetFrameDelay(); void TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, bool useFileSignature); - template void TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, bool useFileSignature); + void TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, ConsoleType consoleType); + template void TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr& console, bool useFileSignature, bool forceConsoleType = false); void InitConsole(unique_ptr& newConsole, ConsoleMemoryInfo originalConsoleMemory[], bool preserveRom); - bool InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom = true, bool forPowerCycle = false); + bool InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom = true, bool forPowerCycle = false, optional forcedConsoleType = std::nullopt); public: Emulator(); @@ -163,7 +164,7 @@ class Emulator void OnBeforePause(bool clearAudioBuffer); - bool LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom = true, bool forPowerCycle = false); + bool LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom = true, bool forPowerCycle = false, optional forcedConsoleType = std::nullopt); RomInfo& GetRomInfo() { return _rom; } string GetHash(HashType type); uint32_t GetCrc32(); @@ -336,4 +337,4 @@ enum class HashType { Sha1, Sha1Cheat -}; \ No newline at end of file +}; diff --git a/Core/Shared/SettingTypes.h b/Core/Shared/SettingTypes.h index 5929cba..49e8fc8 100644 --- a/Core/Shared/SettingTypes.h +++ b/Core/Shared/SettingTypes.h @@ -778,18 +778,12 @@ struct WsConfig uint32_t Channel5Vol = 100; }; -enum class GenesisCoreType -{ - Native = 0 -}; - struct GenesisConfig { ControllerConfig Port1; ControllerConfig Port2; ConsoleRegion Region = ConsoleRegion::Auto; - GenesisCoreType CoreType = GenesisCoreType::Native; }; struct AudioPlayerConfig diff --git a/InteropDLL/EmuApiWrapper.cpp b/InteropDLL/EmuApiWrapper.cpp index 47b69ed..107689b 100644 --- a/InteropDLL/EmuApiWrapper.cpp +++ b/InteropDLL/EmuApiWrapper.cpp @@ -140,6 +140,12 @@ extern "C" { return _emu->LoadRom((VirtualFile)filename, patchFile ? (VirtualFile)patchFile : VirtualFile()); } + DllExport bool __stdcall LoadRomWithConsoleType(char* filename, char* patchFile, ConsoleType consoleType) + { + _emu->GetGameClient()->Disconnect(); + return _emu->LoadRom((VirtualFile)filename, patchFile ? (VirtualFile)patchFile : VirtualFile(), true, false, consoleType); + } + DllExport void __stdcall AddKnownGameFolder(char* folder) { FolderUtilities::AddKnownGameFolder(folder); } DllExport void __stdcall GetRomInfo(InteropRomInfo &info) diff --git a/MCPServer/MCPServer.cpp b/MCPServer/MCPServer.cpp index d67a022..b1257d6 100644 --- a/MCPServer/MCPServer.cpp +++ b/MCPServer/MCPServer.cpp @@ -40,6 +40,94 @@ static const char* PIPE_NAME = "\\\\.\\pipe\\MesenDebug"; static int g_port = 51234; static bool g_stdioMode = false; +static DWORD g_parentPid = 0; +static HANDLE g_singletonMutex = nullptr; +static HANDLE g_parentProcess = nullptr; +static HANDLE g_parentWatcherThread = nullptr; + +static std::string GetExecutablePath() +{ + char path[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH); + if(len == 0 || len >= MAX_PATH) { + return "path\\to\\MCPServer.exe"; + } + + return std::string(path, len); +} + +static DWORD WINAPI ParentWatcherThreadProc(LPVOID) +{ + if(g_parentProcess != nullptr) { + WaitForSingleObject(g_parentProcess, INFINITE); + ExitProcess(0); + } + + return 0; +} + +static bool AcquireSingletonMutex() +{ + static const char* mutexName = "Local\\MesenMcpServerSingleton"; + + g_singletonMutex = CreateMutexA(nullptr, FALSE, mutexName); + if(g_singletonMutex == nullptr) { + fprintf(stderr, "[MCPServer] Failed to create singleton mutex.\n"); + return false; + } + + if(GetLastError() == ERROR_ALREADY_EXISTS) { + fprintf(stderr, "[MCPServer] Another MCP server process is already running.\n"); + CloseHandle(g_singletonMutex); + g_singletonMutex = nullptr; + return false; + } + + return true; +} + +static void ReleaseSingletonMutex() +{ + if(g_singletonMutex != nullptr) { + CloseHandle(g_singletonMutex); + g_singletonMutex = nullptr; + } +} + +static void StopParentWatcher() +{ + if(g_parentWatcherThread != nullptr) { + CloseHandle(g_parentWatcherThread); + g_parentWatcherThread = nullptr; + } + + if(g_parentProcess != nullptr) { + CloseHandle(g_parentProcess); + g_parentProcess = nullptr; + } +} + +static bool StartParentWatcher() +{ + if(g_parentPid == 0) { + return true; + } + + g_parentProcess = OpenProcess(SYNCHRONIZE, FALSE, g_parentPid); + if(g_parentProcess == nullptr) { + fprintf(stderr, "[MCPServer] Failed to open parent process %lu.\n", static_cast(g_parentPid)); + return false; + } + + g_parentWatcherThread = CreateThread(nullptr, 0, ParentWatcherThreadProc, nullptr, 0, nullptr); + if(g_parentWatcherThread == nullptr) { + fprintf(stderr, "[MCPServer] Failed to start parent watcher thread.\n"); + StopParentWatcher(); + return false; + } + + return true; +} // ───────────────────────────────────────────────────────────────────────────── // Named-pipe client @@ -353,14 +441,31 @@ static void HandleClient(SOCKET client) // ───────────────────────────────────────────────────────────────────────────── int main(int argc, char* argv[]) { - if(argc >= 2 && std::string(argv[1]) == "--stdio") { - g_stdioMode = true; - } else if(argc >= 2) { - g_port = std::atoi(argv[1]); + for(int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if(arg == "--stdio") { + g_stdioMode = true; + } else if(arg == "--parent-pid" && i + 1 < argc) { + g_parentPid = static_cast(std::strtoul(argv[++i], nullptr, 10)); + } else { + g_port = std::atoi(argv[i]); + } + } + + if(!AcquireSingletonMutex()) { + return 1; + } + + if(!StartParentWatcher()) { + ReleaseSingletonMutex(); + return 1; } if(g_stdioMode) { - return RunStdio(); + int result = RunStdio(); + StopParentWatcher(); + ReleaseSingletonMutex(); + return result; } printf("[MCPServer] Mesen2 MCP Server\n"); @@ -369,6 +474,8 @@ int main(int argc, char* argv[]) WSADATA wsa{}; if(const int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsa); wsaResult != 0) { fprintf(stderr, "[MCPServer] WSAStartup() failed: %d\n", wsaResult); + StopParentWatcher(); + ReleaseSingletonMutex(); return 1; } InitializeCriticalSection(&g_pipeLock); @@ -381,6 +488,8 @@ int main(int argc, char* argv[]) SOCKET srv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(srv == INVALID_SOCKET) { fprintf(stderr, "[MCPServer] socket() failed: %d\n", WSAGetLastError()); + StopParentWatcher(); + ReleaseSingletonMutex(); return 1; } @@ -394,6 +503,11 @@ int main(int argc, char* argv[]) if(bind(srv, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { fprintf(stderr, "[MCPServer] bind() failed: %d\n", WSAGetLastError()); + closesocket(srv); + WSACleanup(); + DeleteCriticalSection(&g_pipeLock); + StopParentWatcher(); + ReleaseSingletonMutex(); return 1; } listen(srv, SOMAXCONN); @@ -401,6 +515,8 @@ int main(int argc, char* argv[]) printf("[MCPServer] Listening on http://127.0.0.1:%d/mcp/\n", g_port); printf("[MCPServer] To connect from Claude Code:\n"); printf(" claude mcp add --transport http mesen-debugger http://127.0.0.1:%d/mcp/\n", g_port); + printf("[MCPServer] For Codex or other stdio-based clients, use the standalone bridge command instead:\n"); + printf(" codex mcp add mesen-debugger -- \"%s\" --stdio\n", GetExecutablePath().c_str()); while(true) { SOCKET client = accept(srv, nullptr, nullptr); @@ -411,5 +527,7 @@ int main(int argc, char* argv[]) closesocket(srv); WSACleanup(); DeleteCriticalSection(&g_pipeLock); + StopParentWatcher(); + ReleaseSingletonMutex(); return 0; } diff --git a/MCP_SERVER.md b/MCP_SERVER.md index 6a1ef68..7c961e1 100644 --- a/MCP_SERVER.md +++ b/MCP_SERVER.md @@ -9,6 +9,7 @@ The emulator-side debugger integration is designed for local use. External clien Current implementation details visible in the codebase: - Default HTTP endpoint: `http://127.0.0.1:51234/mcp/` +- HTTP port is configurable from the emulator's `Debug > MCP Server` window - Local-only binding by default - Named-pipe bridge support for local tooling @@ -28,7 +29,7 @@ codex mcp add mesen-debugger -- "path\\to\\MCPServer.exe" --stdio ### 2. HTTP bridge -Start the MCP bridge and connect a client to: +Open `Debug > MCP Server`, choose a port, start the bridge, and connect a client to: ```text http://127.0.0.1:51234/mcp/ diff --git a/UI/Assets/McpServer.png b/UI/Assets/McpServer.png new file mode 100644 index 0000000..e25de5e Binary files /dev/null and b/UI/Assets/McpServer.png differ diff --git a/UI/Config/Configuration.cs b/UI/Config/Configuration.cs index 3e5ff24..b6e4d05 100644 --- a/UI/Config/Configuration.cs +++ b/UI/Config/Configuration.cs @@ -47,6 +47,7 @@ public partial class Configuration : ReactiveObject [Reactive] public NetplayConfig Netplay { get; set; } = new(); [Reactive] public HistoryViewerConfig HistoryViewer { get; set; } = new(); [Reactive] public MainWindowConfig MainWindow { get; set; } = new(); + [Reactive] public McpServerConfig McpServer { get; set; } = new(); public DefaultKeyMappingType DefaultKeyMappings { get; set; } = DefaultKeyMappingType.Xbox | DefaultKeyMappingType.ArrowKeys; diff --git a/UI/Config/GenesisConfig.cs b/UI/Config/GenesisConfig.cs index d5e7a79..2538929 100644 --- a/UI/Config/GenesisConfig.cs +++ b/UI/Config/GenesisConfig.cs @@ -13,15 +13,12 @@ public class GenesisConfig : BaseConfig [ValidValues(ConsoleRegion.Auto, ConsoleRegion.Ntsc, ConsoleRegion.NtscJapan, ConsoleRegion.Pal)] [Reactive] public ConsoleRegion Region { get; set; } = ConsoleRegion.Auto; - [Reactive] public GenesisCoreType CoreType { get; set; } = GenesisCoreType.Native; - public void ApplyConfig() { ConfigApi.SetGenesisConfig(new InteropGenesisConfig() { Port1 = Port1.ToInterop(), Port2 = Port2.ToInterop(), - Region = Region, - CoreType = CoreType + Region = Region }); } @@ -39,10 +36,4 @@ public struct InteropGenesisConfig public InteropControllerConfig Port2; public ConsoleRegion Region; - public GenesisCoreType CoreType; -} - -public enum GenesisCoreType -{ - Native = 0 } diff --git a/UI/Config/McpServerConfig.cs b/UI/Config/McpServerConfig.cs new file mode 100644 index 0000000..b8a6d20 --- /dev/null +++ b/UI/Config/McpServerConfig.cs @@ -0,0 +1,9 @@ +using ReactiveUI.Fody.Helpers; + +namespace Mesen.Config +{ + public class McpServerConfig : BaseConfig + { + [Reactive] public ushort Port { get; set; } = 51234; + } +} diff --git a/UI/Config/PreferencesConfig.cs b/UI/Config/PreferencesConfig.cs index 8e10b67..2b6a329 100644 --- a/UI/Config/PreferencesConfig.cs +++ b/UI/Config/PreferencesConfig.cs @@ -22,6 +22,7 @@ namespace Mesen.Config public class PreferencesConfig : BaseConfig { [Reactive] public MesenTheme Theme { get; set; } = MesenTheme.Light; + [Reactive] public string Language { get; set; } = "en"; [Reactive] public bool AutomaticallyCheckForUpdates { get; set; } = true; [Reactive] public bool SingleInstance { get; set; } = true; [Reactive] public bool AutoLoadPatches { get; set; } = true; @@ -92,6 +93,8 @@ public class PreferencesConfig : BaseConfig [Reactive] public string ScreenshotFolder { get; set; } = ""; [Reactive] public string WaveFolder { get; set; } = ""; + [Reactive] public Dictionary RememberedBinFileConsoleTypes { get; set; } = new(); + public PreferencesConfig() { } diff --git a/UI/Debugger/Utilities/ContextMenuAction.cs b/UI/Debugger/Utilities/ContextMenuAction.cs index 64d1adf..eba72bd 100644 --- a/UI/Debugger/Utilities/ContextMenuAction.cs +++ b/UI/Debugger/Utilities/ContextMenuAction.cs @@ -563,6 +563,7 @@ public enum ActionType [IconFile("Settings")] OpenDebugSettings, + [IconFile("McpServer")] OpenMcpServer, [IconFile("SpcDebugger")] diff --git a/UI/Debugger/Utilities/DebugPipeServer.cs b/UI/Debugger/Utilities/DebugPipeServer.cs index c02944d..02033c0 100644 --- a/UI/Debugger/Utilities/DebugPipeServer.cs +++ b/UI/Debugger/Utilities/DebugPipeServer.cs @@ -15,7 +15,9 @@ namespace Mesen.Debugger.Utilities // line → one response line, both compact / newline-free). public class DebugPipeServer { + private readonly object _lock = new(); private Thread? _thread; + private NamedPipeServerStream? _currentPipe; private volatile bool _running; private const string PipeName = "MesenDebug"; private const string ServerName = "Mesen2-MCP"; @@ -32,6 +34,14 @@ public void Start() public void Stop() { _running = false; + lock(_lock) { + _currentPipe?.Dispose(); + _currentPipe = null; + } + try { + _thread?.Join(1000); + } catch { + } } // Accept one client at a time; loop to accept the next after disconnect. @@ -42,6 +52,9 @@ private void Run() try { pipe = new NamedPipeServerStream(PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 65536, 65536); + lock(_lock) { + _currentPipe = pipe; + } pipe.WaitForConnection(); // Use UTF-8 without BOM so every line is plain JSON text. @@ -56,6 +69,11 @@ private void Run() } } catch { } finally { + lock(_lock) { + if(ReferenceEquals(_currentPipe, pipe)) { + _currentPipe = null; + } + } pipe?.Dispose(); } } diff --git a/UI/Debugger/Utilities/McpServerManager.cs b/UI/Debugger/Utilities/McpServerManager.cs new file mode 100644 index 0000000..e47cd50 --- /dev/null +++ b/UI/Debugger/Utilities/McpServerManager.cs @@ -0,0 +1,182 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; + +namespace Mesen.Debugger.Utilities +{ + public static class McpServerManager + { + private static readonly object _lock = new(); + private static readonly string _serverExe = Path.Combine(AppContext.BaseDirectory, "MCPServer.exe"); + private static readonly int _parentProcessId = Process.GetCurrentProcess().Id; + + private static DebugPipeServer? _debugPipeServer; + private static Process? _serverProcess; + private static ushort _port = 51234; + + public static event EventHandler? StateChanged; + + public static ushort Port { + get { + lock(_lock) { + return _port; + } + } + } + + public static string ServerUrl => $"http://127.0.0.1:{Port}/mcp/"; + + public static bool IsRunning { + get { + lock(_lock) { + CleanupExitedProcess_NoLock(); + return IsRunning_NoLock(); + } + } + } + + public static bool TryStart(ushort port, out string error) + { + error = ""; + + lock(_lock) { + CleanupExitedProcess_NoLock(); + + if(IsRunning_NoLock()) { + if(_port == port) { + return true; + } + + error = "The MCP server is already running. Stop it before changing the port."; + return false; + } + + if(!File.Exists(_serverExe)) { + error = $"Could not find MCPServer.exe at:\n{_serverExe}"; + return false; + } + + if(!IsPortAvailable(port)) { + error = $"Port {port} is already in use on localhost."; + return false; + } + + _debugPipeServer ??= new DebugPipeServer(); + _debugPipeServer.Start(); + + var startInfo = new ProcessStartInfo { + FileName = _serverExe, + Arguments = $"{port} --parent-pid {_parentProcessId}", + WorkingDirectory = AppContext.BaseDirectory, + UseShellExecute = false, + CreateNoWindow = false, + WindowStyle = ProcessWindowStyle.Normal + }; + + Process? process = Process.Start(startInfo); + if(process == null) { + _debugPipeServer.Stop(); + _debugPipeServer = null; + error = "Failed to start MCPServer.exe."; + return false; + } + + process.EnableRaisingEvents = true; + process.Exited += ServerProcess_Exited; + if(process.WaitForExit(250)) { + process.Exited -= ServerProcess_Exited; + process.Dispose(); + _debugPipeServer.Stop(); + _debugPipeServer = null; + error = "MCPServer.exe exited immediately. It may already be running, or the selected port may be unavailable."; + return false; + } + + _serverProcess = process; + _port = port; + } + + NotifyStateChanged(); + return true; + } + + public static void Stop() + { + Process? process = null; + + lock(_lock) { + CleanupExitedProcess_NoLock(); + + process = _serverProcess; + _serverProcess = null; + if(process != null) { + process.Exited -= ServerProcess_Exited; + } + + _debugPipeServer?.Stop(); + _debugPipeServer = null; + } + + if(process != null) { + try { + if(!process.HasExited) { + process.Kill(true); + process.WaitForExit(1000); + } + } catch { + } finally { + process.Dispose(); + } + } + + NotifyStateChanged(); + } + + private static void ServerProcess_Exited(object? sender, EventArgs e) + { + lock(_lock) { + CleanupExitedProcess_NoLock(); + _debugPipeServer?.Stop(); + _debugPipeServer = null; + } + + NotifyStateChanged(); + } + + private static bool IsPortAvailable(ushort port) + { + TcpListener? listener = null; + + try { + listener = new TcpListener(IPAddress.Loopback, port); + listener.Start(); + return true; + } catch { + return false; + } finally { + listener?.Stop(); + } + } + + private static bool IsRunning_NoLock() + { + return _serverProcess != null && !_serverProcess.HasExited; + } + + private static void CleanupExitedProcess_NoLock() + { + if(_serverProcess != null && _serverProcess.HasExited) { + _serverProcess.Exited -= ServerProcess_Exited; + _serverProcess.Dispose(); + _serverProcess = null; + } + } + + private static void NotifyStateChanged() + { + StateChanged?.Invoke(null, EventArgs.Empty); + } + } +} diff --git a/UI/Interop/EmuApi.cs b/UI/Interop/EmuApi.cs index e2eaf44..44f13a9 100644 --- a/UI/Interop/EmuApi.cs +++ b/UI/Interop/EmuApi.cs @@ -61,6 +61,12 @@ public static string GetMesenBuildDate() [MarshalAs(UnmanagedType.LPUTF8Str)]string? patchFile = null ); + [DllImport(DllPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool LoadRomWithConsoleType( + [MarshalAs(UnmanagedType.LPUTF8Str)]string filepath, + [MarshalAs(UnmanagedType.LPUTF8Str)]string? patchFile, + ConsoleType consoleType + ); + [DllImport(DllPath, EntryPoint = "GetRomInfo")] private static extern void GetRomInfoWrapper(out InteropRomInfo romInfo); public static RomInfo GetRomInfo() { diff --git a/UI/Localization/ResourceHelper.cs b/UI/Localization/ResourceHelper.cs index 52d5dab..be04307 100644 --- a/UI/Localization/ResourceHelper.cs +++ b/UI/Localization/ResourceHelper.cs @@ -1,15 +1,23 @@ -using System; +using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Xml; +using Mesen.Config; using Mesen.Interop; namespace Mesen.Localization { class ResourceHelper { + private const string DefaultLanguageCode = "en"; + private const string SimplifiedChineseLanguageCode = "zh-cn"; + private const string JapaneseLanguageCode = "ja"; + private const string ResourcePrefix = "Mesen.Localization.resources."; + private const string ResourceSuffix = ".xml"; + private static XmlDocument _resources = new XmlDocument(); private static Dictionary _enumLabelCache = new(); @@ -19,42 +27,131 @@ class ResourceHelper public static void LoadResources() { try { - Assembly assembly = Assembly.GetExecutingAssembly(); + _enumLabelCache.Clear(); + _viewLabelCache.Clear(); + _messageCache.Clear(); - using(StreamReader reader = new StreamReader(assembly.GetManifestResourceStream("Mesen.Localization.resources.en.xml")!)) { - _resources.LoadXml(reader.ReadToEnd()); + LoadResourceFile(DefaultLanguageCode, true); + string language = ConfigManager.Config.Preferences.Language; + if(!string.Equals(language, DefaultLanguageCode, StringComparison.OrdinalIgnoreCase)) { + LoadResourceFile(language, false); } + } catch { + } + } - foreach(XmlNode node in _resources.SelectNodes("/Resources/Messages/Message")!) { - _messageCache[node.Attributes!["ID"]!.Value] = node.InnerText; - } + private static void LoadResourceFile(string language, bool updateResourceDocument) + { + string? resourceData = GetResourceFileContent(language); + if(string.IsNullOrWhiteSpace(resourceData)) { + return; + } + + XmlDocument resources = new XmlDocument(); + resources.LoadXml(resourceData); + if(updateResourceDocument) { + _resources = resources; + } + + foreach(XmlNode node in resources.SelectNodes("/Resources/Messages/Message")!) { + _messageCache[node.Attributes!["ID"]!.Value] = node.InnerText; + } #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - Dictionary enumTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsEnum).ToDictionary(t => t.Name); + Dictionary enumTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsEnum).ToDictionary(t => t.Name); #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - foreach(XmlNode node in _resources.SelectNodes("/Resources/Enums/Enum")!) { - string enumName = node.Attributes!["ID"]!.Value; - if(enumTypes.TryGetValue(enumName, out Type? enumType)) { - foreach(XmlNode enumNode in node.ChildNodes) { - if(Enum.TryParse(enumType, enumNode.Attributes!["ID"]!.Value, out object? value)) { - _enumLabelCache[(Enum)value!] = enumNode.InnerText; - } + foreach(XmlNode node in resources.SelectNodes("/Resources/Enums/Enum")!) { + string enumName = node.Attributes!["ID"]!.Value; + if(enumTypes.TryGetValue(enumName, out Type? enumType)) { + foreach(XmlNode enumNode in node.ChildNodes) { + if(Enum.TryParse(enumType, enumNode.Attributes!["ID"]!.Value, out object? value)) { + _enumLabelCache[(Enum)value!] = enumNode.InnerText; } - } else { - throw new Exception("Unknown enum type: " + enumName); } + } else { + throw new Exception("Unknown enum type: " + enumName); } + } - foreach(XmlNode node in _resources.SelectNodes("/Resources/Forms/Form")!) { - string viewName = node.Attributes!["ID"]!.Value; - foreach(XmlNode formNode in node.ChildNodes) { - if(formNode is XmlElement elem) { - _viewLabelCache[viewName + "_" + elem.Attributes!["ID"]!.Value] = elem.InnerText; - } + foreach(XmlNode node in resources.SelectNodes("/Resources/Forms/Form")!) { + string viewName = node.Attributes!["ID"]!.Value; + foreach(XmlNode formNode in node.ChildNodes) { + if(formNode is XmlElement elem) { + _viewLabelCache[viewName + "_" + elem.Attributes!["ID"]!.Value] = elem.InnerText; } } + } + } + + private static string? GetResourceFileContent(string language) + { + string normalizedLanguage = NormalizeLanguageCode(language); + Assembly assembly = Assembly.GetExecutingAssembly(); + string resourceName = ResourcePrefix + normalizedLanguage + ResourceSuffix; + using(Stream? stream = assembly.GetManifestResourceStream(resourceName)) { + if(stream != null) { + using StreamReader reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } + + string externalPath = Path.Combine(ConfigManager.HomeFolder, "Localization", "resources." + normalizedLanguage + ".xml"); + if(File.Exists(externalPath)) { + return File.ReadAllText(externalPath); + } + + return null; + } + + private static string NormalizeLanguageCode(string language) + { + if(string.IsNullOrWhiteSpace(language)) { + return DefaultLanguageCode; + } + return language.Trim().Replace('_', '-').ToLowerInvariant(); + } + + public static List GetAvailableLanguages() + { + HashSet languages = new() { DefaultLanguageCode, SimplifiedChineseLanguageCode, JapaneseLanguageCode }; + Assembly assembly = Assembly.GetExecutingAssembly(); + foreach(string resourceName in assembly.GetManifestResourceNames()) { + if(resourceName.StartsWith(ResourcePrefix) && resourceName.EndsWith(ResourceSuffix)) { + languages.Add(resourceName.Substring(ResourcePrefix.Length, resourceName.Length - ResourcePrefix.Length - ResourceSuffix.Length)); + } + } + + string externalFolder = Path.Combine(ConfigManager.HomeFolder, "Localization"); + if(Directory.Exists(externalFolder)) { + foreach(string file in Directory.EnumerateFiles(externalFolder, "resources.*.xml")) { + string filename = Path.GetFileName(file); + languages.Add(filename.Substring("resources.".Length, filename.Length - "resources.".Length - ResourceSuffix.Length)); + } + } + + return languages + .Select(language => new LanguageOption(NormalizeLanguageCode(language), GetLanguageDisplayName(language))) + .OrderBy(language => language.DisplayName) + .ToList(); + } + + private static string GetLanguageDisplayName(string language) + { + string normalizedLanguage = NormalizeLanguageCode(language); + if(normalizedLanguage == DefaultLanguageCode) { + return "English"; + } else if(normalizedLanguage == SimplifiedChineseLanguageCode) { + return "中文(简体)"; + } else if(normalizedLanguage == JapaneseLanguageCode) { + return "日本語"; + } + + try { + CultureInfo culture = CultureInfo.GetCultureInfo(normalizedLanguage); + return culture.NativeName; } catch { + return normalizedLanguage; } } @@ -99,4 +196,6 @@ public static string GetViewLabel(string view, string control) } } } + + public record LanguageOption(string Code, string DisplayName); } diff --git a/UI/Localization/resources.en.xml b/UI/Localization/resources.en.xml index d7e7e6e..34d89b7 100644 --- a/UI/Localization/resources.en.xml +++ b/UI/Localization/resources.en.xml @@ -820,6 +820,17 @@ OK Cancel +
+ MCP Server + Start the local HTTP MCP bridge on demand. The bridge binds to localhost and stays attached to this emulator session. + Port: + Copy URL + URL: + Status: + Start + Stop + Close +
Cheats Cheats @@ -974,6 +985,15 @@ OK Cancel
+ +
+ Select Console Type + Select the Console Type to open for this .bin file. + Console Type: + Remember this selection for this .bin file + OK + Cancel +
Import cheats from cheat database... @@ -2564,10 +2584,6 @@ E SMS 2 - - Native - - Auto-detect None @@ -3531,7 +3547,7 @@ E Sprite Viewer Palette Viewer Settings - MCP Server Info + MCP Server Tools History Viewer diff --git a/UI/Localization/resources.ja.xml b/UI/Localization/resources.ja.xml new file mode 100644 index 0000000..3e416df --- /dev/null +++ b/UI/Localization/resources.ja.xml @@ -0,0 +1,228 @@ + + + + + ファイル(_F) + ゲーム(_G) + 設定(_S) + ツール(_T) + デバッグ(_D) + ヘルプ(_H) + + +
+ 全般 + テーマ: + (再起動が必要) + 表示言語: + Mesen のインスタンスを 1 つだけ実行する + 更新を自動的に確認する + 開発者モードを有効にする + + 一時停止 / バックグラウンド設定 + その他の設定 + メニューバーを自動的に隠す + タイトルバーに追加情報を表示する + リセット / 電源再投入 / 電源オフ / 終了の前に確認する + ムービーの再生終了時に一時停止する + ムービーの再生または録画中に再生/一時停止アイコンを表示する + 早送り/巻き戻し中にアイコンを表示する + バックグラウンド時も入力を許可する(コントローラーのみ) + バックグラウンド時に一時停止する + メニューや設定画面を開いている間は一時停止する + デバッグツール + IPS/BPS パッチを自動的に読み込む + 一時停止オーバーレイを隠す + ムービーの再生または録画中に再生/録画アイコンを表示する + + フォルダー / ファイル + ファイルの関連付け + SNES ROM: .sfc, .smc, .swc, .fig, .bs + SNES 音楽: .spc + NES ROM: .nes, .unif, .fds, .qd, .studybox + NES 音楽: .nsf, .nsfe + Game Boy ROM: .gb, .gbc, .gbx + Game Boy 音楽: .gbs + Game Boy Advance ROM: .gba + PC Engine ROM: .pce, .sgx + PC Engine 音楽: .hes + SMS ROM: .sms + Genesis ROM: .md + Game Gear ROM: .gg + SG-1000 ROM: .sg + ColecoVision ROM: .col + WonderSwan ROM: .ws, .wsc + + データ保存フォルダー + フォルダーを変更... + フォルダー: + フォルダーの上書き + ゲーム: + 動画: + スクリーンショット: + セーブデータ: + 音声: + ステートセーブ: + ムービー: + + 詳細 + 最近使ったファイルにフルパスを表示する + フレームカウンターを表示する + ゲームタイマーを表示する + ラグカウンターを表示する + ラグカウンターをリセット + VS System ゲーム読み込み時にゲーム設定ダイアログを表示する + フォント設定 + フォントのアンチエイリアス: + 通常フォント: + メニューフォント: + その他の設定 + + 分ごとにステートセーブを作成する(ゲーム時間) + 巻き戻しに最大 + MB のメモリを使用する(約 5MB/分) + ショートカット + UI 表示設定 + ウィンドウ設定 + マウスでメインウィンドウのサイズを変更しない + ゲーム選択画面の設定 + 前回のセッションを復元せず、電源投入状態からゲームを開始する + ゲーム選択画面: + ゲームデータベースを無効にする + 高精度タイマーを無効にする + 画面メッセージを隠す + 常に最前面に表示する + FPS を表示する + HUD 表示サイズ: + デバッグ情報を表示する + 前回使用したフォルダー + OK + キャンセル +
+ +
+ Console Type を選択 + この .bin ファイルを開く Console Type を選択してください。 + Console Type: + この .bin ファイルの選択を記憶する + OK + キャンセル +
+ +
+ ROM を選択... + 検索: + OK + キャンセル +
+ +
+ データ保存フォルダーを選択 + Mesen データの保存場所を選択してください: + ユーザープロファイル + 「ドキュメント」フォルダーに保存します。 + ポータブルモード + Mesen と同じフォルダーに保存します。 + OK + キャンセル +
+ +
+ 選択... + ファイルを削除 +
+
+ + + 前回のセッション + 自動保存 + スロット #{0} + ステートロードメニュー + ステートセーブメニュー + ファイルから読み込み... + ファイルに保存... + {0} を無効にしました。 + {0} を有効にしました。 + すべての背景/スプライトレイヤーを有効にしました。 + 変更を保存しますか? + 変更を保持しますか? + 2 つ目の Sufami Turbo ROM を読み込み、スロット B に挿入しますか? + このファイルは既知のファームウェアと一致しません。正しく動作しない可能性があります。 期待値 (SHA256): {0} 現在値 (SHA256): {1} + このファイルサイズは必要なサイズと一致しません。 期待値: {0} バイト 現在値: {1} バイト + 次のファイルは完全に削除されます。続行しますか? {0} + 次のファイルは既に存在し、上書きされます。この操作は元に戻せません。続行しますか? {0} + セグメント #{0} + セグメント全体をエクスポート + 指定範囲をエクスポート... + ムービーファイルの保存中にエラーが発生しました。 + ファイルの保存中にエラーが発生しました。 + 新しい割り当てに使うキーを押してください。 + キーボード、コントローラー、またはマウスのボタンを押して新しい割り当てを設定してください。 + 履歴を消去 + このゲームには次のファームウェアファイルが必要です: {0} ファイル名: {1} サイズ: {2} バイト 今すぐ選択しますか? + 選択したファイルは必要なファームウェアと一致しません: {0} ファームウェア SHA-256: {1} 選択したファイル SHA-256: {2} それでもこのファイルを使用しますか? + 選択したファイルは必要なサイズ({0} バイト)ではないため使用できません。 + マウスが有効です - 一時停止するとカーソルを解放します + 右クリックで割り当てを消去 + 再開 + 一時停止 + サーバーを開始 + サーバーを停止 + サーバーに接続 + 切断 + プレイヤー {0} + 拡張デバイス + {0} を押すと全画面表示を終了します + 既定 + {0} 個の ROM が見つかりました + 警告: すべての設定がリセットされ、元に戻せません。 続行しますか? + ファイルが見つかりません: {0} + ファイルを読み込めませんでした: {0} + 予期しないエラーが発生しました: {0} + パッチを適用中: {0} + パッチの適用に失敗しました: {0} + このパッチファイルに使用する ROM を選択してください。 + 現在の ROM にパッチを適用してリセットしますか? + OK + キャンセル + はい + いいえ + + + + + ライト + ダーク + + + 無効 + 状態を復元 + 電源投入 + + + 固定 + 拡大縮小 + + + 無効 + アンチエイリアス + サブピクセルアンチエイリアス + + + 自動(推奨) + PC Engine(日本) + PC Engine SuperGrafx(日本) + TurboGrafx-16(北米) + + + CD-ROM² + Super CD-ROM² + Arcade CD-ROM²(推奨) + + + ランダム値 + すべて 0 + すべて 1 + + +
diff --git a/UI/Localization/resources.zh-cn.xml b/UI/Localization/resources.zh-cn.xml new file mode 100644 index 0000000..e014e7a --- /dev/null +++ b/UI/Localization/resources.zh-cn.xml @@ -0,0 +1,228 @@ + + + + + +
+ 常规 + 主题: + (需要重启) + 显示语言: + 一次只允许运行一个 Mesen 实例 + 自动检查更新 + 启用开发者模式 + + 暂停/后台设置 + 其他设置 + 自动隐藏菜单栏 + 在标题栏显示附加信息 + 重置 / 重新开机 / 关机 / 退出前显示确认对话框 + 录像播放结束后暂停 + 播放或录制录像时显示播放/暂停图标 + 快进/倒带时显示图标 + 后台时允许输入(仅控制器) + 后台时暂停 + 打开菜单和设置对话框时暂停 + 调试工具 + 自动加载 IPS/BPS 补丁 + 隐藏暂停画面 + 播放或录制录像时显示播放/录制图标 + + 文件夹/文件 + 文件关联 + SNES ROM:.sfc, .smc, .swc, .fig, .bs + SNES 音乐:.spc + NES ROM:.nes, .unif, .fds, .qd, .studybox + NES 音乐:.nsf, .nsfe + Game Boy ROM:.gb, .gbc, .gbx + Game Boy 音乐:.gbs + Game Boy Advance ROM:.gba + PC Engine ROM:.pce, .sgx + PC Engine 音乐:.hes + SMS ROM:.sms + Genesis ROM:.md + Game Gear ROM:.gg + SG-1000 ROM:.sg + ColecoVision ROM:.col + WonderSwan ROM:.ws, .wsc + + 数据存储文件夹 + 更改文件夹... + 文件夹: + 文件夹覆盖 + 游戏: + 视频: + 截图: + 存档数据: + 音频: + 即时存档: + 录像: + + 高级 + 在最近文件列表中显示完整路径 + 显示帧计数器 + 显示游戏计时器 + 显示延迟计数器 + 重置延迟计数器 + 加载 VS System 游戏时显示游戏配置对话框 + 字体设置 + 字体抗锯齿: + 默认字体: + 菜单字体: + 其他设置 + 每隔 + 分钟自动创建即时存档(游戏时间) + 允许倒带最多使用 + MB 内存(内存占用约 5MB/分钟) + 快捷键 + 界面显示设置 + 窗口设置 + 不允许用鼠标调整主窗口大小 + 游戏选择界面设置 + 从开机状态启动游戏,而不是恢复上次游玩会话 + 游戏选择界面: + 禁用游戏数据库 + 禁用高精度计时器 + 隐藏屏幕消息 + 始终置顶显示 + 显示 FPS + HUD 显示大小: + 显示调试信息 + 上次使用的文件夹 + 确定 + 取消 +
+ +
+ 选择主机类型 + 选择用于打开此 .bin 文件的主机类型。 + 主机类型: + 记住此 .bin 文件的选择 + 确定 + 取消 +
+ +
+ 选择 ROM... + 搜索: + 确定 + 取消 +
+ +
+ 选择数据存储文件夹 + 选择 Mesen 数据的存储位置: + 用户配置文件 + 存储到“文档”文件夹中。 + 便携模式 + 存储到 Mesen 所在文件夹中。 + 确定 + 取消 +
+ +
+ 选择... + 删除文件 +
+
+ + + 上次会话 + 自动存档 + 槽位 #{0} + 读取即时存档菜单 + 保存即时存档菜单 + 从文件读取... + 保存到文件... + {0} 已禁用。 + {0} 已启用。 + 所有背景/精灵图层已启用。 + 是否保存更改? + 是否保留更改? + 是否加载第二个 Sufami Turbo ROM 并插入到槽位 B? + 此文件与任何已知固件都不匹配,可能无法正常工作。 预期值 (SHA256):{0} 当前值 (SHA256):{1} + 此文件大小不符合要求。 预期:{0} 字节 当前:{1} 字节 + 以下文件将被永久删除。确定要继续吗? {0} + 以下文件已存在并将被覆盖。此操作无法撤销。确定要继续吗? {0} + 片段 #{0} + 导出整个片段 + 导出指定范围... + 尝试保存录像文件时发生错误。 + 尝试保存文件时发生错误。 + 按下键盘上的任意按键来设置新的绑定。 + 按下键盘、控制器或鼠标上的任意按键来设置新的绑定。 + 清除历史记录 + 此游戏需要以下固件文件:{0} 文件名:{1} 大小:{2} 字节 是否现在选择? + 所选文件与所需固件不匹配:{0} 固件 SHA-256: {1} 所选文件 SHA-256: {2} 仍要使用此文件吗? + 所选文件不符合所需大小({0} 字节),无法使用。 + 鼠标已启用 - 暂停以释放光标 + 右键清除绑定 + 继续 + 暂停 + 启动服务器 + 停止服务器 + 连接到服务器 + 断开连接 + 玩家 {0} + 扩展设备 + 按 {0} 退出全屏 + 默认 + 找到 {0} 个 ROM + 警告:这将重置所有设置,且无法撤销! 继续吗? + 找不到文件:{0} + 无法加载文件:{0} + 发生意外错误:{0} + 正在应用补丁:{0} + 补丁应用失败:{0} + 请为此补丁文件选择一个 ROM。 + 是否为当前 ROM 应用补丁并重置? + 确定 + 取消 + + + + + + + 浅色 + 深色 + + + 禁用 + 恢复状态 + 开机启动 + + + 固定 + 缩放 + + + 禁用 + 抗锯齿 + 亚像素抗锯齿 + + + 自动(推荐) + PC Engine(日本) + PC Engine SuperGrafx(日本) + TurboGrafx-16(北美) + + + CD-ROM² + Super CD-ROM² + Arcade CD-ROM²(推荐) + + + 随机值 + 全 0 + 全 1 + + +
diff --git a/UI/UI.csproj b/UI/UI.csproj index 3208ffa..1736a0e 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -507,6 +507,9 @@ SelectRomWindow.axaml + + SelectBinFileConsoleTypeWindow.axaml + NetplayConnectWindow.axaml @@ -641,7 +644,7 @@ - + diff --git a/UI/Utilities/LoadRomHelper.cs b/UI/Utilities/LoadRomHelper.cs index f4c8022..4af3bea 100644 --- a/UI/Utilities/LoadRomHelper.cs +++ b/UI/Utilities/LoadRomHelper.cs @@ -39,17 +39,60 @@ public static async void LoadRom(ResourcePath romPath, ResourcePath? patchPath = } } - InternalLoadRom(romPath, patchPath); + ConsoleType? forcedConsoleType = null; + string? rememberBinFileConfigKey = null; + if(Path.GetExtension(romPath.FileName).Equals("." + FileDialogHelper.BinExt, StringComparison.OrdinalIgnoreCase)) { + string configKey = GetBinFileConfigKey(romPath); + if(ConfigManager.Config.Preferences.RememberedBinFileConsoleTypes.TryGetValue(configKey, out ConsoleType rememberedConsoleType)) { + forcedConsoleType = rememberedConsoleType; + } else { + BinFileConsoleTypeSelection? selection = await SelectBinFileConsoleTypeWindow.Show(romPath); + if(selection == null) { + return; + } + + forcedConsoleType = selection.ConsoleType; + if(selection.RememberSelection) { + rememberBinFileConfigKey = configKey; + } + } + } + + InternalLoadRom(romPath, patchPath, forcedConsoleType, rememberBinFileConfigKey); + } + + private static string GetBinFileConfigKey(ResourcePath romPath) + { + string path = Path.GetFullPath(romPath.Path); + if(OperatingSystem.IsWindows()) { + path = path.ToLowerInvariant(); + } + + if(romPath.Compressed) { + string innerFile = romPath.InnerFile.Replace('\\', '/'); + if(OperatingSystem.IsWindows()) { + innerFile = innerFile.ToLowerInvariant(); + } + return path + "\x1" + innerFile + "\x1" + romPath.InnerFileIndex.ToString(); + } + + return path; } - private static void InternalLoadRom(ResourcePath romPath, ResourcePath? patchPath) + private static void InternalLoadRom(ResourcePath romPath, ResourcePath? patchPath, ConsoleType? forcedConsoleType = null, string? rememberBinFileConfigKey = null) { //Temporarily hide selection screen to allow displaying error messages MainWindowViewModel.Instance.RecentGames.Visible = false; Task.Run(() => { //Run in another thread to prevent deadlocks etc. when emulator notifications are processed UI-side - if(EmuApi.LoadRom(romPath, patchPath)) { + bool loaded = forcedConsoleType.HasValue ? + EmuApi.LoadRomWithConsoleType(romPath, patchPath, forcedConsoleType.Value) : + EmuApi.LoadRom(romPath, patchPath); + if(loaded) { + if(rememberBinFileConfigKey != null && forcedConsoleType.HasValue) { + ConfigManager.Config.Preferences.RememberedBinFileConsoleTypes[rememberBinFileConfigKey] = forcedConsoleType.Value; + } ConfigManager.Config.RecentFiles.AddRecentFile(romPath, patchPath); ConfigManager.Config.Save(); } diff --git a/UI/ViewModels/MainMenuViewModel.cs b/UI/ViewModels/MainMenuViewModel.cs index 08643ea..0ab137f 100644 --- a/UI/ViewModels/MainMenuViewModel.cs +++ b/UI/ViewModels/MainMenuViewModel.cs @@ -1129,22 +1129,9 @@ private void InitDebugMenu(Window wnd) DebugShortcutManager.RegisterActions(wnd, DebugMenuItems); } - private static readonly string McpServerUrl = "http://127.0.0.1:51234/mcp/"; - private static readonly string McpServerExe = Path.Combine(AppContext.BaseDirectory, "MCPServer.exe"); - - private async void OpenMcpServer(Window wnd) + private void OpenMcpServer(Window wnd) { - ApplicationHelper.GetMainWindow()?.Clipboard?.SetTextAsync(McpServerUrl); - - string bridgePath = File.Exists(McpServerExe) ? McpServerExe : "path\\to\\MCPServer.exe"; - - await MessageBox.Show( - wnd, - $"MCP bridge URL copied to clipboard:\n\n{McpServerUrl}\n\nMesen Expanded exposes the debugger over a named pipe when the main window is open.\n\nFor Codex, use the stdio bridge:\n codex mcp add mesen-debugger -- \"{bridgePath}\" --stdio\n\nFor Claude Code over HTTP, start MCPServer.exe and run:\n claude mcp add --transport http mesen-debugger {McpServerUrl}\n\nFor Claude Desktop, start MCPServer.exe and add:\n {{\"mcpServers\":{{\"mesen-debugger\":{{\"url\":\"{McpServerUrl}\"}}}}}}", - "Mesen Expanded - MCP Server", - MessageBoxButtons.OK, - MessageBoxIcon.Info - ); + ApplicationHelper.GetOrCreateUniqueWindow((Control)wnd, () => new McpServerWindow()); } private void InitHelpMenu(Window wnd) diff --git a/UI/ViewModels/McpServerWindowViewModel.cs b/UI/ViewModels/McpServerWindowViewModel.cs new file mode 100644 index 0000000..67be188 --- /dev/null +++ b/UI/ViewModels/McpServerWindowViewModel.cs @@ -0,0 +1,76 @@ +using Avalonia.Threading; +using Mesen.Config; +using Mesen.Debugger.Utilities; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Reactive.Linq; + +namespace Mesen.ViewModels +{ + public class McpServerWindowViewModel : DisposableViewModel + { + [Reactive] public int Port { get; set; } + [Reactive] public bool IsRunning { get; private set; } + [Reactive] public string ServerUrl { get; private set; } = ""; + [Reactive] public string Status { get; private set; } = ""; + [Reactive] public bool CanEditPort { get; private set; } + [Reactive] public bool CanStart { get; private set; } + [Reactive] public bool CanStop { get; private set; } + + public McpServerWindowViewModel() + { + Port = ConfigManager.Config.McpServer.Port; + RefreshDerivedState(); + + AddDisposable(this.WhenAnyValue(x => x.Port).Subscribe(_ => RefreshDerivedState())); + McpServerManager.StateChanged += McpServerManager_StateChanged; + } + + public bool TryStart(out string error) + { + error = ""; + + if(Port < 1 || Port > 65535) { + error = "Port must be between 1 and 65535."; + return false; + } + + if(!McpServerManager.TryStart((ushort)Port, out error)) { + RefreshDerivedState(); + return false; + } + + ConfigManager.Config.McpServer.Port = (ushort)Port; + ConfigManager.Config.Save(); + RefreshDerivedState(); + return true; + } + + public void Stop() + { + McpServerManager.Stop(); + RefreshDerivedState(); + } + + protected override void DisposeView() + { + McpServerManager.StateChanged -= McpServerManager_StateChanged; + } + + private void McpServerManager_StateChanged(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(RefreshDerivedState); + } + + private void RefreshDerivedState() + { + IsRunning = McpServerManager.IsRunning; + ServerUrl = $"http://127.0.0.1:{Port}/mcp/"; + Status = IsRunning ? $"Running on {McpServerManager.ServerUrl}" : "Stopped"; + CanEditPort = !IsRunning; + CanStart = !IsRunning; + CanStop = IsRunning; + } + } +} diff --git a/UI/ViewModels/PreferencesConfigViewModel.cs b/UI/ViewModels/PreferencesConfigViewModel.cs index 4dd04ad..a27d59f 100644 --- a/UI/ViewModels/PreferencesConfigViewModel.cs +++ b/UI/ViewModels/PreferencesConfigViewModel.cs @@ -3,10 +3,13 @@ using Avalonia.Styling; using Mesen.Config; using Mesen.Config.Shortcuts; +using Mesen.Localization; using Mesen.Utilities; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; +using System.Linq; namespace Mesen.ViewModels { @@ -19,6 +22,8 @@ public class PreferencesConfigViewModel : DisposableViewModel public bool IsOsx { get; } public List ShortcutKeys { get; set; } + public List AvailableLanguages { get; } + [Reactive] public LanguageOption? SelectedLanguage { get; set; } public PreferencesConfigViewModel() { @@ -27,6 +32,13 @@ public PreferencesConfigViewModel() IsOsx = OperatingSystem.IsMacOS(); DataStorageLocation = ConfigManager.HomeFolder; + AvailableLanguages = ResourceHelper.GetAvailableLanguages(); + SelectedLanguage = AvailableLanguages.FirstOrDefault(language => language.Code == Config.Language) ?? AvailableLanguages.FirstOrDefault(); + AddDisposable(this.WhenAnyValue(x => x.SelectedLanguage).Subscribe(language => { + if(language != null) { + Config.Language = language.Code; + } + })); EmulatorShortcut[] displayOrder = new EmulatorShortcut[] { EmulatorShortcut.FastForward, diff --git a/UI/Views/PreferencesConfigView.axaml b/UI/Views/PreferencesConfigView.axaml index 6fd1596..89273d6 100644 --- a/UI/Views/PreferencesConfigView.axaml +++ b/UI/Views/PreferencesConfigView.axaml @@ -26,6 +26,17 @@ + + + + + + + + + + + diff --git a/UI/Windows/MainWindow.axaml.cs b/UI/Windows/MainWindow.axaml.cs index 7b0d3b1..b3d760c 100644 --- a/UI/Windows/MainWindow.axaml.cs +++ b/UI/Windows/MainWindow.axaml.cs @@ -64,7 +64,6 @@ public class MainWindow : MesenWindow private Dictionary _pendingKeyUpEvents = new(); private bool _isLinux = false; - private DebugPipeServer? _debugPipeServer; private Stopwatch _stopWatch = Stopwatch.StartNew(); private Dictionary _keyPressedStamp = new(); private bool _focusInMenu; @@ -163,6 +162,7 @@ private bool CloseEmu(bool force) } _timerBackgroundFlag.Stop(); + McpServerManager.Stop(); EmuApi.Stop(); _listener?.Dispose(); EmuApi.Release(); @@ -258,9 +258,6 @@ protected override void OnOpened(EventArgs e) _model.Init(this); - _debugPipeServer = new DebugPipeServer(); - _debugPipeServer.Start(); - ConfigManager.Config.ApplyConfig(); if(ConfigManager.Config.Preferences.OverrideGameFolder && Directory.Exists(ConfigManager.Config.Preferences.GameFolder)) { diff --git a/UI/Windows/McpServerWindow.axaml b/UI/Windows/McpServerWindow.axaml new file mode 100644 index 0000000..b9f3ac8 --- /dev/null +++ b/UI/Windows/McpServerWindow.axaml @@ -0,0 +1,46 @@ + + + +