diff --git a/Core/Debugger/Debugger.cpp b/Core/Debugger/Debugger.cpp index 906d1ffe2..3ada08239 100644 --- a/Core/Debugger/Debugger.cpp +++ b/Core/Debugger/Debugger.cpp @@ -216,6 +216,9 @@ bool Debugger::ProcessStepBack(IDebugger* debugger) template void Debugger::ProcessInstruction() { + if(_emu->IsDebuggerDisabled()) { + return; + } IDebugger* debugger = _debuggers[(int)type].Debugger.get(); if(debugger->IsStepBack() && ProcessStepBack(debugger)) { debugger->AllowChangeProgramCounter = true; //set to true temporarily to allow debugger to pause on break requests when rewinding/step back is active @@ -256,6 +259,9 @@ void Debugger::ProcessInstruction() template void Debugger::ProcessMemoryRead(uint32_t addr, T& value, MemoryOperationType opType) { + if(_emu->IsDebuggerDisabled()) { + return; + } if(_debuggers[(int)type].Debugger->IsStepBack()) { SleepOnBreakRequest(); return; @@ -289,6 +295,9 @@ void Debugger::ProcessMemoryRead(uint32_t addr, T& value, MemoryOperationType op template bool Debugger::ProcessMemoryWrite(uint32_t addr, T& value, MemoryOperationType opType) { + if(_emu->IsDebuggerDisabled()) { + return true; + } if(_debuggers[(int)type].Debugger->IsStepBack()) { SleepOnBreakRequest(); return !_debuggers[(int)type].Debugger->GetFrozenAddressManager().IsFrozenAddress(addr); @@ -328,7 +337,7 @@ void Debugger::ProcessMemoryAccess(uint32_t addr, T& value) constexpr int accessWidth = std::is_same::value ? 2 : 1; - if(debugger->IsStepBack()) { + if(debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } @@ -357,6 +366,9 @@ void Debugger::ProcessMemoryAccess(uint32_t addr, T& value) template void Debugger::ProcessIdleCycle() { + if(_emu->IsDebuggerDisabled()) { + return; + } if(_debuggers[(int)type].Debugger->IsStepBack()) { SleepOnBreakRequest(); return; @@ -375,6 +387,9 @@ template void Debugger::ProcessHaltedCpu() { IDebugger* dbg = _debuggers[(int)type].Debugger.get(); + if(_emu->IsDebuggerDisabled()) { + return; + } if(dbg->IsStepBack() && ProcessStepBack(dbg)) { dbg->AllowChangeProgramCounter = true; //set to true temporarily to allow debugger to pause on break requests when rewinding/step back is active SleepOnBreakRequest(); @@ -410,7 +425,7 @@ void Debugger::SleepOnBreakRequest() template void Debugger::ProcessPpuRead(uint16_t addr, T& value, MemoryType memoryType, MemoryOperationType opType) { - if(_debuggers[(int)type].Debugger->IsStepBack()) { + if(_debuggers[(int)type].Debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } @@ -431,7 +446,7 @@ void Debugger::ProcessPpuRead(uint16_t addr, T& value, MemoryType memoryType, Me template void Debugger::ProcessPpuWrite(uint16_t addr, T& value, MemoryType memoryType) { - if(_debuggers[(int)type].Debugger->IsStepBack()) { + if(_debuggers[(int)type].Debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } @@ -452,7 +467,7 @@ void Debugger::ProcessPpuWrite(uint16_t addr, T& value, MemoryType memoryType) template void Debugger::ProcessPpuCycle() { - if(_debuggers[(int)type].Debugger->IsStepBack()) { + if(_debuggers[(int)type].Debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } @@ -571,7 +586,7 @@ void Debugger::ProcessPredictiveBreakpoint(CpuType sourceCpu, BreakpointManager* template void Debugger::ProcessInterrupt(uint32_t originalPc, uint32_t currentPc, bool forNmi) { - if(_debuggers[(int)type].Debugger->IsStepBack()) { + if(_debuggers[(int)type].Debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } @@ -826,7 +841,7 @@ bool Debugger::IsBreakOptionEnabled(BreakSource src) void Debugger::BreakImmediately(CpuType sourceCpu, BreakSource source) { - if(_debuggers[(int)sourceCpu].Debugger->IsStepBack()) { + if(_debuggers[(int)sourceCpu].Debugger->IsStepBack() || _emu->IsDebuggerDisabled()) { return; } diff --git a/Core/Gameboy/APU/GbApu.cpp b/Core/Gameboy/APU/GbApu.cpp index 1763fac39..de6199c6a 100644 --- a/Core/Gameboy/APU/GbApu.cpp +++ b/Core/Gameboy/APU/GbApu.cpp @@ -9,8 +9,8 @@ GbApu::GbApu() { - _soundBuffer = new int16_t[GbApu::MaxSamples * 2]; - memset(_soundBuffer, 0, GbApu::MaxSamples * 2 * sizeof(int16_t)); + _soundBuffer = new int16_t[GbApu::MaxSamples * 2 * 2]; // *2 for stereo, then *2 as a precaution against buffer overflows + memset(_soundBuffer, 0, GbApu::MaxSamples * 2 * 2 * sizeof(int16_t)); _leftChannel = blip_new(GbApu::MaxSamples); _rightChannel = blip_new(GbApu::MaxSamples); @@ -27,6 +27,7 @@ void GbApu::Init(Emulator* emu, Gameboy* gameboy) _prevRightOutput = 0; _clockCounter = 0; _prevClockCount = 0; + _sampleCount = 0; _emu = emu; _settings = emu->GetSettings(); @@ -109,7 +110,7 @@ void GbApu::Run() } } - if(!_gameboy->IsSgb() && _clockCounter >= 20000) { + if(!_gameboy->IsSgb() && _clockCounter >= 20000 && !_gameboy->IsPrimaryConsole()) { PlayQueuedAudio(); } } @@ -148,10 +149,60 @@ void GbApu::PlayQueuedAudio() blip_end_frame(_leftChannel, _clockCounter); blip_end_frame(_rightChannel, _clockCounter); - uint32_t sampleCount = (uint32_t)blip_read_samples(_leftChannel, _soundBuffer, GbApu::MaxSamples, 1); - blip_read_samples(_rightChannel, _soundBuffer + 1, GbApu::MaxSamples, 1); - _soundMixer->PlayAudioBuffer(_soundBuffer, sampleCount, GbApu::SampleRate); + int16_t* out = _soundBuffer + (_sampleCount * 2); + size_t sampleCount = blip_read_samples(_leftChannel, out, GbApu::MaxSamples, 1); + blip_read_samples(_rightChannel, out + 1, GbApu::MaxSamples, 1); + _sampleCount += sampleCount; + if(_sampleCount > GbApu::MaxSamples) { // Hacky safeguard against buffer overflows + _sampleCount = GbApu::MaxSamples; + } _clockCounter = 0; + + if(!_gameboy->IsPrimaryConsole()) { + // For the secondary console, leave the data in the buffer for the primary console to mix into its own audio + return; + } + + GameboyConfig& cfg = _emu->GetSettings()->GetGameboyConfig(); + if(_gameboy->IsPrimaryConsole() && _gameboy->GetLinkedConsole()) { + ProcessLinkCableAudio(); + } + + _soundMixer->PlayAudioBuffer(_soundBuffer, (uint32_t)_sampleCount, GbApu::SampleRate); + + _sampleCount = 0; +} + +void GbApu::ProcessLinkCableAudio() +{ + GameboyConfig& cfg = _emu->GetSettings()->GetGameboyConfig(); + + if(cfg.LocalLinkCableAudioOutput == GbLocalLinkOutputOption::SubSystemOnly) { + //Mute the main system's sound + memset(_soundBuffer, 0, _sampleCount * sizeof(int16_t) * 2); + } + + GbApu* subApu = _gameboy->GetLinkedConsole()->GetApu(); + subApu->Run(); + subApu->PlayQueuedAudio(); + + if(cfg.LocalLinkCableAudioOutput != GbLocalLinkOutputOption::MainSystemOnly) { + size_t i; + for(i = 0; i < _sampleCount && i < subApu->_sampleCount; i++) { + _soundBuffer[i * 2] += subApu->_soundBuffer[i * 2]; + _soundBuffer[i * 2 + 1] += subApu->_soundBuffer[i * 2 + 1]; + } + + if(i < subApu->_sampleCount) { + size_t samplesToCopy = subApu->_sampleCount - i; + memmove(subApu->_soundBuffer, subApu->_soundBuffer + i * 2, samplesToCopy * 2 * sizeof(int16_t)); + subApu->_sampleCount = samplesToCopy; + } else { + subApu->_sampleCount = 0; + } + } else { + subApu->_sampleCount = 0; + } } void GbApu::GetSoundSamples(int16_t*& samples, uint32_t& sampleCount) @@ -464,6 +515,7 @@ void GbApu::Serialize(Serializer& s) Run(); } else { _clockCounter = 0; + _sampleCount = 0; blip_clear(_leftChannel); blip_clear(_rightChannel); } diff --git a/Core/Gameboy/APU/GbApu.h b/Core/Gameboy/APU/GbApu.h index 7d9ab4786..02def3608 100644 --- a/Core/Gameboy/APU/GbApu.h +++ b/Core/Gameboy/APU/GbApu.h @@ -33,6 +33,7 @@ class GbApu : public ISerializable int16_t* _soundBuffer = nullptr; blip_t* _leftChannel = nullptr; blip_t* _rightChannel = nullptr; + size_t _sampleCount = 0; int16_t _prevLeftOutput = 0; int16_t _prevRightOutput = 0; @@ -64,6 +65,8 @@ class GbApu : public ISerializable void PlayQueuedAudio(); + void ProcessLinkCableAudio(); + void GetSoundSamples(int16_t*& samples, uint32_t& sampleCount); void ClockFrameSequencer(); diff --git a/Core/Gameboy/Gameboy.cpp b/Core/Gameboy/Gameboy.cpp index 6c43b3a95..737b08255 100644 --- a/Core/Gameboy/Gameboy.cpp +++ b/Core/Gameboy/Gameboy.cpp @@ -156,13 +156,16 @@ void Gameboy::Run(uint64_t runUntilClock) { while(_cpu->GetCycleCount() < runUntilClock) { _cpu->Exec(); + if(_secondaryConsole) { + RunLinkedConsole(); + } } } void Gameboy::LoadBattery() { if(_hasBattery) { - _emu->GetBatteryManager()->LoadBattery(".srm", _cartRam, _cartRamSize); + _emu->GetBatteryManager()->LoadBattery(IsPrimaryConsole() ? ".srm" : ".p2.srm", _cartRam, _cartRamSize); } } @@ -170,6 +173,11 @@ void Gameboy::SaveBattery() { if(_hasBattery) { _emu->GetBatteryManager()->SaveBattery(".srm", _cartRam, _cartRamSize); + + // SaveBattery only gets called on the primary console, so write the secondary console's save too + if(_secondaryConsole) { + _emu->GetBatteryManager()->SaveBattery(".p2.srm", _secondaryConsole->_cartRam, _secondaryConsole->_cartRamSize); + } } _cart->SaveBattery(); } @@ -232,6 +240,11 @@ Emulator* Gameboy::GetEmulator() return _emu; } +GbApu* Gameboy::GetApu() +{ + return _apu.get(); +} + GbPpu* Gameboy::GetPpu() { return _ppu.get(); @@ -333,6 +346,14 @@ SuperGameboy* Gameboy::GetSgb() return _superGameboy; } +Gameboy* Gameboy::GetLinkedConsole() +{ + if(_secondaryConsole) { + return _secondaryConsole.get(); + } + return _mainConsole; +} + uint64_t Gameboy::GetCycleCount() { return _cpu->GetCycleCount(); @@ -343,6 +364,11 @@ uint64_t Gameboy::GetApuCycleCount() return _memoryManager->GetApuCycleCount(); } +bool Gameboy::IsPrimaryConsole() +{ + return _mainConsole == nullptr; +} + void Gameboy::Serialize(Serializer& s) { SV(_cpu); @@ -361,6 +387,10 @@ void Gameboy::Serialize(Serializer& s) SVArray(_highRam, Gameboy::HighRamSize); SV(_controlManager); + + if(_secondaryConsole) { + SV(_secondaryConsole); + } } SaveStateCompatInfo Gameboy::ValidateSaveStateCompatibility(ConsoleType stateConsoleType) @@ -461,6 +491,22 @@ LoadRomResult Gameboy::LoadRom(VirtualFile& romFile) } else { Init(cart, romData, header.GetCartRamSize(), header.HasBattery()); } + + EmuSettings* settings = _emu->GetSettings(); + GameboyConfig cfg = settings->GetGameboyConfig(); + if(!_mainConsole && cfg.UseLocalLinkCable && !_allowSgb) { // Don't allow link cable with SGB for now + _emu->SetDebuggerDisabled(true); + + // Create second console and link it with this one + _secondaryConsole.reset(new Gameboy(_emu)); + _secondaryConsole->_mainConsole = this; + LoadRomResult result = _secondaryConsole->LoadRom(romFile); + + _emu->SetDebuggerDisabled(false); + if(result != LoadRomResult::Success) { + return result; + } + } return LoadRomResult::Success; } else { MessageManager::DisplayMessage("Error", "Unsupported cart type: " + (gbxFooter.IsValid() ? gbxFooter.GetMapperId() : std::to_string(header.CartType))); @@ -562,14 +608,34 @@ GameboyModel Gameboy::GetEffectiveModel(GameboyHeader& header) void Gameboy::RunFrame() { uint32_t frameCount = _ppu->GetFrameCount(); + while(frameCount == _ppu->GetFrameCount()) { _cpu->Exec(); + if(_secondaryConsole) { + RunLinkedConsole(); + } } _apu->Run(); _apu->PlayQueuedAudio(); } +void Gameboy::RunLinkedConsole() +{ + _emu->SetDebuggerDisabled(true); + int64_t cycleGap; + while(true) { + //Run the sub console until it catches up to the main CPU + cycleGap = (int64_t)(_cpu->GetCycleCount() - _secondaryConsole->_cpu->GetCycleCount()); + if(cycleGap > 5 || _ppu->GetFrameCount() > _secondaryConsole->_ppu->GetFrameCount()) { + _secondaryConsole->_cpu->Exec(); + } else { + break; + } + } + _emu->SetDebuggerDisabled(false); +} + void Gameboy::ProcessEndOfFrame() { _controlManager->UpdateControlDevices(); diff --git a/Core/Gameboy/Gameboy.h b/Core/Gameboy/Gameboy.h index 0fd916c18..27ff427a3 100644 --- a/Core/Gameboy/Gameboy.h +++ b/Core/Gameboy/Gameboy.h @@ -30,6 +30,9 @@ class Gameboy final : public IConsole SuperGameboy* _superGameboy = nullptr; bool _allowSgb = false; + unique_ptr _secondaryConsole; + Gameboy* _mainConsole = nullptr; + unique_ptr _memoryManager; unique_ptr _cpu; unique_ptr _ppu; @@ -83,6 +86,7 @@ class Gameboy final : public IConsole Emulator* GetEmulator(); + GbApu* GetApu(); GbPpu* GetPpu(); GbCpu* GetCpu(); GbTimer* GetTimer(); @@ -101,6 +105,7 @@ class Gameboy final : public IConsole bool IsCgb(); bool IsSgb(); SuperGameboy* GetSgb(); + Gameboy* GetLinkedConsole(); uint64_t GetCycleCount(); uint64_t GetApuCycleCount(); @@ -109,6 +114,9 @@ class Gameboy final : public IConsole void RunApu(); + void RunLinkedConsole(); + bool IsPrimaryConsole(); + void Serialize(Serializer& s) override; SaveStateCompatInfo ValidateSaveStateCompatibility(ConsoleType stateConsoleType) override; diff --git a/Core/Gameboy/GbConstants.h b/Core/Gameboy/GbConstants.h index 03ee00fa1..fad135e66 100644 --- a/Core/Gameboy/GbConstants.h +++ b/Core/Gameboy/GbConstants.h @@ -7,4 +7,5 @@ class GbConstants static constexpr uint32_t ScreenWidth = 160; static constexpr uint32_t ScreenHeight = 144; static constexpr uint32_t PixelCount = GbConstants::ScreenWidth * GbConstants::ScreenHeight; + static constexpr uint32_t LinkedPixelCount = GbConstants::PixelCount * 2; }; \ No newline at end of file diff --git a/Core/Gameboy/GbControlManager.cpp b/Core/Gameboy/GbControlManager.cpp index 97ae7a510..3ae98ddd3 100644 --- a/Core/Gameboy/GbControlManager.cpp +++ b/Core/Gameboy/GbControlManager.cpp @@ -14,6 +14,15 @@ GbControlManager::GbControlManager(Emulator* emu, Gameboy* console) : BaseContro { _emu = emu; _console = console; + + if(!console->IsPrimaryConsole()) { + RegisterInputProvider(this); + } +} + +GbControlManager::~GbControlManager() +{ + UnregisterInputProvider(this); } GbControlManagerState GbControlManager::GetState() @@ -35,7 +44,7 @@ shared_ptr GbControlManager::CreateControllerDevice(Controlle default: case ControllerType::None: break; - case ControllerType::GameboyController: device.reset(new GbController(_emu, port, cfg.Controller.Keys)); break; + case ControllerType::GameboyController: device.reset(new GbController(_emu, port, port == 0 ? cfg.Controller.Keys : cfg.LinkedController.Keys)); break; } return device; @@ -57,6 +66,14 @@ void GbControlManager::UpdateControlDevices() if(device) { RegisterControlDevice(device); } + + if(_console->IsPrimaryConsole() && _console->GetLinkedConsole()) { + shared_ptr linkedDevice = CreateControllerDevice(ControllerType::GameboyController, 1); + if(linkedDevice) { + RegisterControlDevice(linkedDevice); + linkedDevice->Disconnect(); + } + } } uint8_t GbControlManager::ReadInputPort() @@ -132,6 +149,22 @@ void GbControlManager::UpdateInputState() ProcessInputChange([this]() { BaseControlManager::UpdateInputState(); }); } +bool GbControlManager::SetInput(BaseControlDevice* device) +{ + //Copy port P2 (port 1) from the main console to the subconsole. + //This allows input to be recorded properly for rewind, movies, etc. + uint8_t port = device->GetPort(); + GbControlManager* mainControlManager = (GbControlManager*)_console->GetLinkedConsole()->GetControlManager(); + if(mainControlManager && port == 0) { + shared_ptr controlDevice = mainControlManager->GetControlDevice(1); + if(controlDevice) { + ControlDeviceState state = controlDevice->GetRawState(); + device->SetRawState(state); + } + } + return true; +} + void GbControlManager::Serialize(Serializer& s) { BaseControlManager::Serialize(s); diff --git a/Core/Gameboy/GbControlManager.h b/Core/Gameboy/GbControlManager.h index ecb8c0992..a820179fd 100644 --- a/Core/Gameboy/GbControlManager.h +++ b/Core/Gameboy/GbControlManager.h @@ -2,13 +2,14 @@ #include "pch.h" #include #include "Shared/BaseControlManager.h" +#include "Shared/Interfaces/IInputProvider.h" #include "Shared/SettingTypes.h" class Emulator; class Gameboy; class BaseControlDevice; -class GbControlManager final : public BaseControlManager +class GbControlManager final : public BaseControlManager, public IInputProvider { private: Emulator* _emu = nullptr; @@ -18,6 +19,7 @@ class GbControlManager final : public BaseControlManager public: GbControlManager(Emulator* emu, Gameboy* console); + ~GbControlManager(); GbControlManagerState GetState(); @@ -30,5 +32,7 @@ class GbControlManager final : public BaseControlManager void UpdateInputState() override; + bool SetInput(BaseControlDevice* device) override; + void Serialize(Serializer& s) override; }; \ No newline at end of file diff --git a/Core/Gameboy/GbDefaultVideoFilter.cpp b/Core/Gameboy/GbDefaultVideoFilter.cpp index cbe60b2ae..ac5f6fd34 100644 --- a/Core/Gameboy/GbDefaultVideoFilter.cpp +++ b/Core/Gameboy/GbDefaultVideoFilter.cpp @@ -13,8 +13,8 @@ GbDefaultVideoFilter::GbDefaultVideoFilter(Emulator* emu, bool applyNtscFilter) { InitLookupTable(); _applyNtscFilter = applyNtscFilter; - _prevFrame = new uint16_t[GbConstants::PixelCount]; - memset(_prevFrame, 0, GbConstants::PixelCount * sizeof(uint16_t)); + _prevFrame = new uint16_t[GbConstants::LinkedPixelCount]; + memset(_prevFrame, 0, GbConstants::LinkedPixelCount * sizeof(uint16_t)); } GbDefaultVideoFilter::~GbDefaultVideoFilter() @@ -88,7 +88,7 @@ void GbDefaultVideoFilter::OnBeforeApplyFilter() bool blendFrames = gbConfig.BlendFrames && !_emu->GetRewindManager()->IsRewinding() && !_emu->IsPaused(); if(_blendFrames != blendFrames) { _blendFrames = blendFrames; - memset(_prevFrame, 0, GbConstants::PixelCount * sizeof(uint16_t)); + memset(_prevFrame, 0, GbConstants::LinkedPixelCount * sizeof(uint16_t)); } _videoConfig = config; } @@ -98,21 +98,22 @@ void GbDefaultVideoFilter::ApplyFilter(uint16_t* ppuOutputBuffer) if(_emu->GetRomInfo().Format == RomFormat::Gbs) { return; } + FrameInfo frame = _baseFrameInfo; uint32_t* out = GetOutputBuffer(); - for(uint32_t i = 0; i < GbConstants::ScreenHeight; i++) { - for(uint32_t j = 0; j < GbConstants::ScreenWidth; j++) { - out[i * GbConstants::ScreenWidth + j] = GetPixel(ppuOutputBuffer, i * GbConstants::ScreenWidth + j); + for(uint32_t i = 0; i < frame.Height; i++) { + for(uint32_t j = 0; j < frame.Width; j++) { + out[i * _baseFrameInfo.Width + j] = GetPixel(ppuOutputBuffer, i * _baseFrameInfo.Width + j); } } if(_blendFrames) { - std::copy(ppuOutputBuffer, ppuOutputBuffer + GbConstants::PixelCount, _prevFrame); + std::copy(ppuOutputBuffer, ppuOutputBuffer + (frame.Width * frame.Height), _prevFrame); } if(_applyNtscFilter) { - _ntscFilter.ApplyFilter(out, GbConstants::ScreenWidth, GbConstants::ScreenHeight, 0); + _ntscFilter.ApplyFilter(out, frame.Width, frame.Height, 0); } } diff --git a/Core/Gameboy/GbMemoryManager.cpp b/Core/Gameboy/GbMemoryManager.cpp index 85f2b7dff..a4ebfe3cd 100644 --- a/Core/Gameboy/GbMemoryManager.cpp +++ b/Core/Gameboy/GbMemoryManager.cpp @@ -85,16 +85,8 @@ void GbMemoryManager::ExecTimerDmaSerial() _dmaController->Exec(); } - if(_state.SerialBitCount && (_cpu->GetState().CycleCount & 0x1FF) == 0) { - _state.SerialData = (_state.SerialData << 1) | 0x01; - if(--_state.SerialBitCount == 0) { - //"It will be notified that the transfer is complete in two ways: - //SC's Bit 7 will be cleared" - _state.SerialControl &= 0x7F; - - //"and the Serial Interrupt handler will be called" - RequestIrq(GbIrqSource::Serial); - } + if(_state.SerialBitCount && ((_state.SerialControl & 0x81) == 0x81) && (_cpu->GetState().CycleCount & ((_state.SerialControl & 0x2) ? 0xF : 0x1FF)) == 0) { + RunSerialTransfer(); } } @@ -472,7 +464,7 @@ void GbMemoryManager::WriteRegister(uint16_t addr, uint8_t value) case 0xFF02: //FF02 - SC - Serial Transfer Control (R/W) _state.SerialControl = value & (_gameboy->IsCgb() ? 0x83 : 0x81); - if((_state.SerialControl & 0x80) && (_state.SerialControl & 0x01)) { + if(_state.SerialControl & 0x80) { _state.SerialBitCount = 8; } else { _state.SerialBitCount = 0; @@ -598,6 +590,38 @@ uint64_t GbMemoryManager::GetApuCycleCount() return _state.ApuCycleCount; } +void GbMemoryManager::RunSerialTransfer() +{ + _state.MostRecentSerialBit = (_state.SerialData & 0x80) == 0x80; + if(_gameboy->GetLinkedConsole() != nullptr) { + _state.SerialData = (_state.SerialData << 1) | (_gameboy->GetLinkedConsole()->GetMemoryManager()->ExchangeSerialBits(_state.MostRecentSerialBit) ? 0x01 : 0x00); + } else { + _state.SerialData = (_state.SerialData << 1) | 0x01; + } + if(--_state.SerialBitCount == 0) { + //"It will be notified that the transfer is complete in two ways: + //SC's Bit 7 will be cleared" + _state.SerialControl &= 0x7F; + + //"and the Serial Interrupt handler will be called" + RequestIrq(GbIrqSource::Serial); + } +} + +bool GbMemoryManager::ExchangeSerialBits(bool serialBit) +{ + if(_state.SerialBitCount && ((_state.SerialControl & 0x81) == 0x80)) { + _state.MostRecentSerialBit = _state.SerialData & 0x80; + _state.SerialData = (_state.SerialData << 1) | (serialBit ? 0x01 : 0x00); + + if(--_state.SerialBitCount == 0) { + _state.SerialControl &= 0x7F; + RequestIrq(GbIrqSource::Serial); + } + } + return _state.MostRecentSerialBit; +} + void GbMemoryManager::Serialize(Serializer& s) { SV(_state.DisableBootRom); @@ -610,6 +634,7 @@ void GbMemoryManager::Serialize(Serializer& s) SV(_state.SerialData); SV(_state.SerialControl); SV(_state.SerialBitCount); + SV(_state.MostRecentSerialBit); SV(_state.CgbRegFF72); SV(_state.CgbRegFF73); SV(_state.CgbRegFF74); diff --git a/Core/Gameboy/GbMemoryManager.h b/Core/Gameboy/GbMemoryManager.h index 5a0b3a2c7..ec10e6a9d 100644 --- a/Core/Gameboy/GbMemoryManager.h +++ b/Core/Gameboy/GbMemoryManager.h @@ -83,6 +83,9 @@ class GbMemoryManager : public ISerializable uint64_t GetApuCycleCount(); + void RunSerialTransfer(); + bool ExchangeSerialBits(bool serialBit); + uint8_t DebugRead(uint16_t addr); void DebugWrite(uint16_t addr, uint8_t value); diff --git a/Core/Gameboy/GbPpu.cpp b/Core/Gameboy/GbPpu.cpp index 619ce9530..9b0b5e2a8 100644 --- a/Core/Gameboy/GbPpu.cpp +++ b/Core/Gameboy/GbPpu.cpp @@ -28,6 +28,7 @@ void GbPpu::Init(Emulator* emu, Gameboy* gameboy, GbMemoryManager* memoryManager _dmaController = dmaController; _vram = vram; _oam = oam; + _settings = _emu->GetSettings(); _outputBuffers[0] = new uint16_t[GbConstants::PixelCount]; _outputBuffers[1] = new uint16_t[GbConstants::PixelCount]; @@ -807,7 +808,9 @@ void GbPpu::SendFrame() UpdatePalette(); - _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone); + if(_gameboy->IsPrimaryConsole()) { + _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone); + } if(_forceBlankFrame) { //Send blank frame on the first frame after enabling LCD @@ -820,15 +823,57 @@ void GbPpu::SendFrame() _isFirstFrame = false; RenderedFrame frame(_currentBuffer, GbConstants::ScreenWidth, GbConstants::ScreenHeight, 1.0, _state.FrameCount, _gameboy->GetControlManager()->GetPortStates()); - bool rewinding = _emu->GetRewindManager()->IsRewinding(); - _emu->GetVideoDecoder()->UpdateFrame(frame, rewinding, rewinding); - _emu->ProcessEndOfFrame(); + if(_gameboy->GetLinkedConsole()) { + SendLinkedFrame(); + if(_gameboy->IsPrimaryConsole()) { + _emu->ProcessEndOfFrame(); + } + } else { + bool rewinding = _emu->GetRewindManager()->IsRewinding(); + _emu->GetVideoDecoder()->UpdateFrame(frame, rewinding, rewinding); + _emu->ProcessEndOfFrame(); + } + _gameboy->ProcessEndOfFrame(); _currentBuffer = _currentBuffer == _outputBuffers[0] ? _outputBuffers[1] : _outputBuffers[0]; } +void GbPpu::SendLinkedFrame() +{ + GameboyConfig& cfg = _settings->GetGameboyConfig(); + bool forRewind = _emu->GetRewindManager()->IsRewinding(); + + RenderedFrame frame(_currentBuffer, GbConstants::ScreenWidth, GbConstants::ScreenHeight, 1.0, _state.FrameCount, _gameboy->GetControlManager()->GetPortStates()); + + if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::MainSystemOnly && _gameboy->IsPrimaryConsole()) { + _emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind); + } else if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::SubSystemOnly && !_gameboy->IsPrimaryConsole()) { + _emu->GetVideoDecoder()->UpdateFrame(frame, forRewind, forRewind); + } else if(cfg.LocalLinkCableVideoOutput == GbLocalLinkOutputOption::Both) { + if(_gameboy->IsPrimaryConsole()) { + uint16_t* mergedBuffer = new uint16_t[GbConstants::ScreenWidth * GbConstants::ScreenHeight * 2]; + + uint16_t* in1 = _currentBuffer; + uint16_t* in2 = ((GbPpu*)_gameboy->GetLinkedConsole()->GetPpu())->_currentBuffer; + uint16_t* out = mergedBuffer; + for(int i = 0; i < GbConstants::ScreenHeight; i++) { + memcpy(out, in1, GbConstants::ScreenWidth * sizeof(uint16_t)); + out += GbConstants::ScreenWidth; + in1 += GbConstants::ScreenWidth; + memcpy(out, in2, GbConstants::ScreenWidth * sizeof(uint16_t)); + out += GbConstants::ScreenWidth; + in2 += GbConstants::ScreenWidth; + } + + RenderedFrame mergedFrame(mergedBuffer, GbConstants::ScreenWidth * 2, GbConstants::ScreenHeight, 1.0, _state.FrameCount, _gameboy->GetControlManager()->GetPortStates()); + _emu->GetVideoDecoder()->UpdateFrame(mergedFrame, true, forRewind); + delete[] mergedBuffer; + } + } +} + void GbPpu::DebugSendFrame() { if(_gameboy->IsSgb()) { diff --git a/Core/Gameboy/GbPpu.h b/Core/Gameboy/GbPpu.h index 9f5cfa6a2..b624aa67c 100644 --- a/Core/Gameboy/GbPpu.h +++ b/Core/Gameboy/GbPpu.h @@ -7,6 +7,7 @@ class Emulator; class Gameboy; class GbMemoryManager; class GbDmaController; +class EmuSettings; class GbPpu : public ISerializable { @@ -18,7 +19,7 @@ class GbPpu : public ISerializable GbDmaController* _dmaController = nullptr; uint16_t* _outputBuffers[2] = {}; uint16_t* _currentBuffer = nullptr; - + EmuSettings* _settings = nullptr; uint16_t* _eventViewerBuffers[2] = {}; uint16_t* _currentEventViewerBuffer = nullptr; EvtColor _evtColor = EvtColor::HBlank; @@ -96,6 +97,7 @@ class GbPpu : public ISerializable __forceinline uint16_t LcdReadObjPalette(uint8_t addr); void SendFrame(); + void SendLinkedFrame(); void UpdatePalette(); void SetMode(PpuMode mode); diff --git a/Core/Gameboy/GbTypes.h b/Core/Gameboy/GbTypes.h index deeb0d705..3db4052da 100644 --- a/Core/Gameboy/GbTypes.h +++ b/Core/Gameboy/GbTypes.h @@ -390,6 +390,7 @@ struct GbMemoryManagerState uint8_t SerialData; uint8_t SerialControl; uint8_t SerialBitCount; + bool MostRecentSerialBit; bool IsReadRegister[0x100]; bool IsWriteRegister[0x100]; diff --git a/Core/Shared/EmuSettings.cpp b/Core/Shared/EmuSettings.cpp index 55e8cc08d..6f975a040 100644 --- a/Core/Shared/EmuSettings.cpp +++ b/Core/Shared/EmuSettings.cpp @@ -115,6 +115,7 @@ void EmuSettings::Serialize(Serializer& s) SV(_gameboy.Controller.Type); SV(_gameboy.Model); SV(_gameboy.UseSgb2); + SV(_gameboy.UseLocalLinkCable); break; case ConsoleType::PcEngine: diff --git a/Core/Shared/Emulator.cpp b/Core/Shared/Emulator.cpp index 73656ad63..f194019d8 100644 --- a/Core/Shared/Emulator.cpp +++ b/Core/Shared/Emulator.cpp @@ -1115,6 +1115,9 @@ void Emulator::SetStopCode(int32_t stopCode) void Emulator::RegisterMemory(MemoryType type, void* memory, uint32_t size) { + if(_isDebuggerDisabled) { + return; + } _consoleMemory[(int)type] = { memory, size }; } @@ -1169,6 +1172,16 @@ void Emulator::BreakIfDebugging(CpuType sourceCpu, BreakSource source) } } +bool Emulator::IsDebuggerDisabled() +{ + return _isDebuggerDisabled; +} + +void Emulator::SetDebuggerDisabled(bool value) +{ + _isDebuggerDisabled = value; +} + template void Emulator::AddDebugEvent(DebugEventType evtType); template void Emulator::AddDebugEvent(DebugEventType evtType); template void Emulator::AddDebugEvent(DebugEventType evtType); diff --git a/Core/Shared/Emulator.h b/Core/Shared/Emulator.h index 0e00b22bd..fca2d956f 100644 --- a/Core/Shared/Emulator.h +++ b/Core/Shared/Emulator.h @@ -119,6 +119,8 @@ class Emulator int32_t _stopCode = 0; bool _stopRequested = false; + bool _isDebuggerDisabled = false; + void WaitForLock(); void WaitForPauseEnd(); @@ -330,6 +332,9 @@ class Emulator void ProcessEvent(EventType type, std::optional cpuType = std::nullopt); template void AddDebugEvent(DebugEventType evtType); void BreakIfDebugging(CpuType sourceCpu, BreakSource source); + + bool IsDebuggerDisabled(); + void SetDebuggerDisabled(bool value); }; enum class HashType diff --git a/Core/Shared/SettingTypes.h b/Core/Shared/SettingTypes.h index 3403dac13..2c5eee862 100644 --- a/Core/Shared/SettingTypes.h +++ b/Core/Shared/SettingTypes.h @@ -402,13 +402,25 @@ struct GameConfig OverscanDimensions Overscan = {}; }; +enum class GbLocalLinkOutputOption +{ + Both = 0, + MainSystemOnly = 1, + SubSystemOnly = 2 +}; + struct GameboyConfig { ControllerConfig Controller; + ControllerConfig LinkedController; GameboyModel Model = GameboyModel::AutoFavorGbc; bool UseSgb2 = true; + bool UseLocalLinkCable = false; + GbLocalLinkOutputOption LocalLinkCableVideoOutput = GbLocalLinkOutputOption::Both; + GbLocalLinkOutputOption LocalLinkCableAudioOutput = GbLocalLinkOutputOption::Both; + bool BlendFrames = true; bool GbcAdjustColors = true; diff --git a/UI/Config/GameboyConfig.cs b/UI/Config/GameboyConfig.cs index ca39919ee..b096cb04a 100644 --- a/UI/Config/GameboyConfig.cs +++ b/UI/Config/GameboyConfig.cs @@ -14,10 +14,15 @@ public class GameboyConfig : BaseConfig [Reactive] public ConsoleOverrideConfig ConfigOverrides { get; set; } = new(); [Reactive] public ControllerConfig Controller { get; set; } = new(); + [Reactive] public ControllerConfig LinkedController { get; set; } = new(); [Reactive] public GameboyModel Model { get; set; } = GameboyModel.AutoFavorBest; [Reactive] public bool UseSgb2 { get; set; } = true; + [Reactive] public bool UseLocalLinkCable { get; set; } = true; + [Reactive] public GbLocalLinkOutputOption LocalLinkCableVideoOutput { get; set; } = GbLocalLinkOutputOption.Both; + [Reactive] public GbLocalLinkOutputOption LocalLinkCableAudioOutput { get; set; } = GbLocalLinkOutputOption.Both; + [Reactive] public bool BlendFrames { get; set; } = true; [Reactive] public bool GbcAdjustColors { get; set; } = true; @@ -43,9 +48,14 @@ public void ApplyConfig() ConfigApi.SetGameboyConfig(new InteropGameboyConfig() { Controller = Controller.ToInterop(), + LinkedController = LinkedController.ToInterop(), Model = Model, UseSgb2 = UseSgb2, + UseLocalLinkCable = UseLocalLinkCable, + LocalLinkCableVideoOutput = LocalLinkCableVideoOutput, + LocalLinkCableAudioOutput = LocalLinkCableAudioOutput, + BlendFrames = BlendFrames, GbcAdjustColors = GbcAdjustColors, DisableBackground = DisableBackground, @@ -76,10 +86,15 @@ internal void InitializeDefaults(DefaultKeyMappingType defaultMappings) public struct InteropGameboyConfig { public InteropControllerConfig Controller; + public InteropControllerConfig LinkedController; public GameboyModel Model; [MarshalAs(UnmanagedType.I1)] public bool UseSgb2; + [MarshalAs(UnmanagedType.I1)] public bool UseLocalLinkCable; + public GbLocalLinkOutputOption LocalLinkCableVideoOutput; + public GbLocalLinkOutputOption LocalLinkCableAudioOutput; + [MarshalAs(UnmanagedType.I1)] public bool BlendFrames; [MarshalAs(UnmanagedType.I1)] public bool GbcAdjustColors; @@ -105,6 +120,13 @@ public struct InteropGameboyConfig public UInt32 WaveVol; } + public enum GbLocalLinkOutputOption + { + Both = 0, + MainSystemOnly = 1, + SubSystemOnly = 2 + } + public enum GameboyModel { AutoFavorBest, diff --git a/UI/Interop/ConsoleState/GbState.cs b/UI/Interop/ConsoleState/GbState.cs index 1bc88b79b..4cf7cfec7 100644 --- a/UI/Interop/ConsoleState/GbState.cs +++ b/UI/Interop/ConsoleState/GbState.cs @@ -42,6 +42,7 @@ public struct GbMemoryManagerState public byte SerialData; public byte SerialControl; public byte SerialBitCount; + [MarshalAs(UnmanagedType.I1)] public bool MostRecentSerialBit; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] public byte[] IsReadRegister; diff --git a/UI/Localization/resources.en.xml b/UI/Localization/resources.en.xml index cadb77149..917c80026 100644 --- a/UI/Localization/resources.en.xml +++ b/UI/Localization/resources.en.xml @@ -402,7 +402,14 @@ Input Controllers Player 1 + Player 2 + (Used for link cable emulation) Setup + + Emulate two linked Game Boy consoles + Link cable emulation + Play audio for: + Show video for:
@@ -2522,6 +2529,11 @@ E Panning Comb filter + + Both + Player 1 only + Player 2 only + Both Main system only diff --git a/UI/ViewModels/GameboyConfigViewModel.cs b/UI/ViewModels/GameboyConfigViewModel.cs index b595aaf23..4a8c51e3b 100644 --- a/UI/ViewModels/GameboyConfigViewModel.cs +++ b/UI/ViewModels/GameboyConfigViewModel.cs @@ -18,6 +18,7 @@ public class GameboyConfigViewModel : DisposableViewModel [Reactive] public GameboyConfigTab SelectedTab { get; set; } = 0; public ReactiveCommand SetupPlayer { get; } + public ReactiveCommand SetupPlayer2 { get; } public GameboyConfigViewModel() { @@ -26,6 +27,7 @@ public GameboyConfigViewModel() IObservable button1Enabled = this.WhenAnyValue(x => x.Config.Controller.Type, x => x.CanConfigure()); SetupPlayer = ReactiveCommand.Create