From 1946187fa2041a9a54435f06fee6641f28408a55 Mon Sep 17 00:00:00 2001 From: NovaSquirrel Date: Fri, 3 Apr 2026 17:21:02 -0400 Subject: [PATCH 1/9] GB: Link cable support with two local consoles --- Core/Debugger/Debugger.cpp | 27 ++++++++--- Core/Gameboy/APU/GbApu.cpp | 49 +++++++++++++++++-- Core/Gameboy/APU/GbApu.h | 3 ++ Core/Gameboy/Gameboy.cpp | 63 ++++++++++++++++++++++++- Core/Gameboy/Gameboy.h | 8 ++++ Core/Gameboy/GbConstants.h | 1 + Core/Gameboy/GbControlManager.cpp | 2 +- Core/Gameboy/GbDefaultVideoFilter.cpp | 17 +++---- Core/Gameboy/GbMemoryManager.cpp | 27 +++++++++-- Core/Gameboy/GbMemoryManager.h | 2 + Core/Gameboy/GbPpu.cpp | 53 +++++++++++++++++++-- Core/Gameboy/GbPpu.h | 4 +- Core/Gameboy/GbTypes.h | 1 + Core/Shared/EmuSettings.cpp | 1 + Core/Shared/Emulator.cpp | 13 +++++ Core/Shared/Emulator.h | 5 ++ Core/Shared/SettingTypes.h | 12 +++++ UI/Config/GameboyConfig.cs | 15 ++++++ UI/Config/NesConfig.cs | 7 +++ UI/Interop/ConsoleState/GbState.cs | 1 + UI/Localization/resources.en.xml | 12 +++++ UI/ViewModels/GameboyConfigViewModel.cs | 12 +++-- UI/Views/GameboyConfigView.axaml | 35 +++++++++++++- 23 files changed, 337 insertions(+), 33 deletions(-) diff --git a/Core/Debugger/Debugger.cpp b/Core/Debugger/Debugger.cpp index b8fe28e34..459882cce 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 374f57af3..c4d23de0a 100644 --- a/Core/Gameboy/APU/GbApu.cpp +++ b/Core/Gameboy/APU/GbApu.cpp @@ -27,6 +27,7 @@ void GbApu::Init(Emulator* emu, Gameboy* gameboy) _prevRightOutput = 0; _clockCounter = 0; _prevClockCount = 0; + _sampleCount = 0; _emu = emu; _settings = emu->GetSettings(); @@ -146,10 +147,52 @@ 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; _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(); + if(cfg.LocalLinkCableAudioOutput != GbLocalLinkOutputOption::MainSystemOnly) { + size_t i; + for(i = 0; i < _sampleCount && 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; + } } void GbApu::GetSoundSamples(int16_t* &samples, uint32_t& sampleCount) diff --git a/Core/Gameboy/APU/GbApu.h b/Core/Gameboy/APU/GbApu.h index 47eac8642..2537c59d6 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 00f02b145..e8bc3285f 100644 --- a/Core/Gameboy/Gameboy.cpp +++ b/Core/Gameboy/Gameboy.cpp @@ -156,6 +156,9 @@ void Gameboy::Run(uint64_t runUntilClock) { while(_cpu->GetCycleCount() < runUntilClock) { _cpu->Exec(); + if(_secondaryConsole) { + RunLinkedConsole(); + } } } @@ -232,6 +235,11 @@ Emulator* Gameboy::GetEmulator() return _emu; } +GbApu* Gameboy::GetApu() +{ + return _apu.get(); +} + GbPpu* Gameboy::GetPpu() { return _ppu.get(); @@ -333,6 +341,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 +359,11 @@ uint64_t Gameboy::GetApuCycleCount() return _memoryManager->GetApuCycleCount(); } +bool Gameboy::IsPrimaryConsole() +{ + return _mainConsole == nullptr; +} + void Gameboy::Serialize(Serializer& s) { SV(_cpu); @@ -361,6 +382,10 @@ void Gameboy::Serialize(Serializer& s) SVArray(_highRam, Gameboy::HighRamSize); SV(_controlManager); + + if(_secondaryConsole) { + SV(_secondaryConsole); + } } SaveStateCompatInfo Gameboy::ValidateSaveStateCompatibility(ConsoleType stateConsoleType) @@ -400,7 +425,7 @@ LoadRomResult Gameboy::LoadRom(VirtualFile& romFile) //Pad to multiple of 16kb gbsRomData.insert(gbsRomData.end(), 0x4000 - (gbsRomData.size() & 0x3FFF), 0); } - + MessageManager::Log("-----------------------------"); MessageManager::Log("File: " + romFile.GetFileName()); @@ -461,6 +486,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) { + _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 +603,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 61aa6780d..da7043ebc 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 20e57c271..fa9ef6034 100644 --- a/Core/Gameboy/GbControlManager.cpp +++ b/Core/Gameboy/GbControlManager.cpp @@ -35,7 +35,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, _console->IsPrimaryConsole() ? cfg.Controller.Keys : cfg.LinkedController.Keys)); break; } return device; diff --git a/Core/Gameboy/GbDefaultVideoFilter.cpp b/Core/Gameboy/GbDefaultVideoFilter.cpp index b6098ff9f..d0fb25356 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 + GbConstants::LinkedPixelCount, _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 1a43ae72b..e72d09ae3 100644 --- a/Core/Gameboy/GbMemoryManager.cpp +++ b/Core/Gameboy/GbMemoryManager.cpp @@ -85,8 +85,13 @@ void GbMemoryManager::ExecTimerDmaSerial() _dmaController->Exec(); } - if(_state.SerialBitCount && (_cpu->GetState().CycleCount & 0x1FF) == 0) { - _state.SerialData = (_state.SerialData << 1) | 0x01; + if(_state.SerialBitCount && ((_state.SerialControl & 0x81) == 0x81) && (_cpu->GetState().CycleCount & 0x1FF) == 0) { + _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" @@ -454,7 +459,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; @@ -577,11 +582,25 @@ uint64_t GbMemoryManager::GetApuCycleCount() return _state.ApuCycleCount; } +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); SV(_state.IrqEnabled); SV(_state.IrqRequests); SV(_state.ApuCycleCount); SV(_state.CgbHighSpeed); SV(_state.CgbSwitchSpeedRequest); SV(_state.CgbWorkRamBank); - SV(_state.SerialData); SV(_state.SerialControl); SV(_state.SerialBitCount); + SV(_state.SerialData); SV(_state.SerialControl); SV(_state.SerialBitCount); SV(_state.MostRecentSerialBit); SV(_state.CgbRegFF72); SV(_state.CgbRegFF73); SV(_state.CgbRegFF74); SV(_state.CgbRegFF75); SV(_state.CgbRegRpInfrared); diff --git a/Core/Gameboy/GbMemoryManager.h b/Core/Gameboy/GbMemoryManager.h index 7d417b4ea..a8005ecfe 100644 --- a/Core/Gameboy/GbMemoryManager.h +++ b/Core/Gameboy/GbMemoryManager.h @@ -82,6 +82,8 @@ class GbMemoryManager : public ISerializable bool IsBootRomDisabled(); uint64_t GetApuCycleCount(); + + 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 8afc3d49e..3c3189f19 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]; @@ -806,7 +807,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 @@ -819,15 +822,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 f02bb5aba..8df31a2b0 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 87d734c89..28e35b381 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 f14451106..b76f5cef2 100644 --- a/Core/Shared/EmuSettings.cpp +++ b/Core/Shared/EmuSettings.cpp @@ -91,6 +91,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 70e9240ac..d0ef09d6f 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 0076ad9b9..982036516 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 f72132693..fdb52845b 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 814f9343b..74e17016c 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; diff --git a/UI/Config/NesConfig.cs b/UI/Config/NesConfig.cs index 08b317c4a..1c06792d0 100644 --- a/UI/Config/NesConfig.cs +++ b/UI/Config/NesConfig.cs @@ -405,6 +405,13 @@ public enum VsDualOutputOption SubSystemOnly = 2 } + public enum GbLocalLinkOutputOption + { + Both = 0, + MainSystemOnly = 1, + SubSystemOnly = 2 + } + public enum NesConsoleType { Nes001, 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..5ab91b31f 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 + First system only + Second system only + Both Main system only diff --git a/UI/ViewModels/GameboyConfigViewModel.cs b/UI/ViewModels/GameboyConfigViewModel.cs index b595aaf23..b13a35af2 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