Skip to content

Commit

Permalink
SNES: Improved gamepad auto-read emulation based on research/test rom
Browse files Browse the repository at this point in the history
  • Loading branch information
SourMesen committed Dec 14, 2024
1 parent 0352c7d commit 6b77200
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 62 deletions.
113 changes: 57 additions & 56 deletions Core/SNES/InternalRegisters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,89 +44,97 @@ void InternalRegisters::Reset()
_irqFlag = false;
_autoReadClockStart = 0;
_autoReadNextClock = 0;
_autoReadActive = false;
_autoReadPort1Value = 0;
_autoReadPort2Value = 0;
}

void InternalRegisters::SetAutoJoypadReadClock()
{
//Auto-read starts at the first multiple of 256 master clocks after dot 32.5 (hclock 130)
uint64_t rangeStart = _console->GetMasterClock() + 130;
_autoReadClockStart = rangeStart + ((rangeStart & 0xFF) ? (256 - (rangeStart & 0xFF)) : 0);
_autoReadClockStart = rangeStart + ((rangeStart & 0xFF) ? (256 - (rangeStart & 0xFF)) : 0) - 128;
_autoReadNextClock = _autoReadClockStart;
}

void InternalRegisters::ProcessAutoJoypad()
{
if(_autoReadNextClock == 0) {
if(_autoReadClockStart == 0) {
return;
}

for(uint64_t clock = _autoReadNextClock; clock <= _console->GetMasterClock(); clock += 128) {
_autoReadNextClock = clock + 128;
uint64_t masterClock = _console->GetMasterClock();
if(masterClock < _autoReadClockStart) {
return;
}

if(!_state.EnableAutoJoypadRead) {
continue;
}
if((masterClock - _autoReadClockStart) < 256) {
//If EnableAutoJoypadRead changes at any point in the first 256 clocks of this process,
//the value sent to OUT0 can be changed and immediately strobe the controllers, etc.
_controlManager->SetAutoReadStrobe(_state.EnableAutoJoypadRead);
}

for(uint64_t clock = _autoReadNextClock; clock <= _console->GetMasterClock(); clock += 128) {
_autoReadNextClock = clock + 128;
int step = (clock - _autoReadClockStart) / 128;

switch(step) {
case 0:
//If bit 0 was set to 1 by a CPU write, auto-read can't set the value back to 0
//causing the controllers to continuously report the value of the B button
if((_controlManager->GetLastWriteValue() & 0x01) == 0) {
_controlManager->Write(0x4016, 1, true);
}
case 0:
//Strobe starts 128 cycles before the auto-read flag is set
_controlManager->SetAutoReadStrobe(_state.EnableAutoJoypadRead);
break;

case 1:
//If bit 0 was set to 1 by a CPU write, auto-read can't set the value back to 0
//causing the controllers to continuously report the value of the B button
if((_controlManager->GetLastWriteValue() & 0x01) == 0) {
_controlManager->Write(0x4016, 0, true);
if(!_state.EnableAutoJoypadRead) {
//Skip auto-read for this frame if the flag is not enabled at this point
_autoReadNextClock = 0;
_autoReadClockStart = 0;
_autoReadActive = false;
} else {
//Set the active flag, reset the registers to 0 (only if auto-read is enabled)
_autoReadActive = true;

_state.ControllerData[0] = 0;
_state.ControllerData[1] = 0;
_state.ControllerData[2] = 0;
_state.ControllerData[3] = 0;
}
break;

_state.ControllerData[0] = 0;
_state.ControllerData[1] = 0;
_state.ControllerData[2] = 0;
_state.ControllerData[3] = 0;
case 2:
//Strobe ends after 256 clock cycles
_controlManager->SetAutoReadStrobe(false);
break;

default:
if((step & 0x01) == 0) {
uint8_t port1 = _controlManager->Read(0x4016, true);
uint8_t port2 = _controlManager->Read(0x4017, true);

if((step & 0x01) == 1) {
//First half of the 256-clock cycle reads the ports
_autoReadPort1Value = _controlManager->Read(0x4016, true);
_autoReadPort2Value = _controlManager->Read(0x4017, true);
} else {
//Second half shifts the data and inserts the new bit at position 0
_state.ControllerData[0] <<= 1;
_state.ControllerData[1] <<= 1;
_state.ControllerData[2] <<= 1;
_state.ControllerData[3] <<= 1;

_state.ControllerData[0] |= (port1 & 0x01);
_state.ControllerData[1] |= (port2 & 0x01);
_state.ControllerData[2] |= (port1 & 0x02) >> 1;
_state.ControllerData[3] |= (port2 & 0x02) >> 1;
_state.ControllerData[0] |= (_autoReadPort1Value & 0x01);
_state.ControllerData[1] |= (_autoReadPort2Value & 0x01);
_state.ControllerData[2] |= (_autoReadPort1Value & 0x02) >> 1;
_state.ControllerData[3] |= (_autoReadPort2Value & 0x02) >> 1;
}
break;
}

if(step > 32) {
if(step >= 34) {
//Last tick done, disable auto-read
_autoReadClockStart = 0;
_autoReadNextClock = 0;
_autoReadActive = false;
break;
}
}
}

bool InternalRegisters::IsAutoReadActive()
{
uint64_t masterClock = _console->GetMasterClock();
return (
_state.EnableAutoJoypadRead &&
_autoReadClockStart > 0 &&
masterClock >= _autoReadClockStart &&
masterClock <= _autoReadClockStart + 128 * 33 //4224 master clocks
);
}

uint8_t InternalRegisters::ReadControllerData(uint8_t port, bool getMsb)
{
ProcessAutoJoypad();
Expand All @@ -141,7 +149,7 @@ uint8_t InternalRegisters::ReadControllerData(uint8_t port, bool getMsb)
value = (uint8_t)_state.ControllerData[port];
}

if(IsAutoReadActive()) {
if(_autoReadActive) {
//TODO add a break option for this?
_console->GetEmulator()->DebugLog("[Input] Read input during auto-read - results may be invalid.");
}
Expand Down Expand Up @@ -215,14 +223,16 @@ uint8_t InternalRegisters::Read(uint16_t addr)
}

case 0x4212: {
ProcessAutoJoypad();

uint16_t hClock = _memoryManager->GetHClock();
uint16_t scanline = _ppu->GetScanline();
uint16_t nmiScanline = _ppu->GetNmiScanline();
//TODO TIMING (set/clear timing)
return (
(scanline >= nmiScanline ? 0x80 : 0) |
((hClock >= 1*4 && hClock <= 274*4) ? 0 : 0x40) |
(IsAutoReadActive() ? 0x01 : 0) | //Auto joypad read in progress
(_autoReadActive ? 0x01 : 0) | //Auto joypad read in progress
(_memoryManager->GetOpenBus() & 0x3E)
);
}
Expand Down Expand Up @@ -260,18 +270,6 @@ void InternalRegisters::Write(uint16_t addr, uint8_t value)
bool autoRead = (value & 0x01) != 0;
if(_state.EnableAutoJoypadRead != autoRead) {
ProcessAutoJoypad();
if(!autoRead) {
_autoReadClockStart = 0;
_autoReadNextClock = 0;
} else if(_autoReadClockStart != 0 && _autoReadClockStart <= _console->GetMasterClock()) {
//If enable flag was enabled after the first clock of the process, skip auto-read for this frame
//Pocky & Rocky seems to enable auto-read in the middle of the auto-read process (scanline ~226)
//in some scenarios (e.g: when player 2 presses Y+Left at the same time) and if the auto-read
//starts in the middle of its process, this corrupts input in a way that the game does not expect.
//TODO determine the exact behavior when auto-read is enabled/disabled mid-way through the auto-read "portion" or the frame
_autoReadClockStart = 0;
_autoReadNextClock = 0;
}
}

_state.EnableNmi = (value & 0x80) != 0;
Expand Down Expand Up @@ -357,4 +355,7 @@ void InternalRegisters::Serialize(Serializer &s)

SV(_autoReadClockStart);
SV(_autoReadNextClock);
SV(_autoReadActive);
SV(_autoReadPort1Value);
SV(_autoReadPort2Value);
}
4 changes: 3 additions & 1 deletion Core/SNES/InternalRegisters.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ class InternalRegisters final : public ISerializable

uint64_t _autoReadClockStart = 0;
uint64_t _autoReadNextClock = 0;
bool _autoReadActive = false;
uint8_t _autoReadPort1Value = 0;
uint8_t _autoReadPort2Value = 0;

void SetIrqFlag(bool irqFlag);

bool IsAutoReadActive();
uint8_t ReadControllerData(uint8_t port, bool getMsb);

public:
Expand Down
24 changes: 20 additions & 4 deletions Core/SNES/SnesControlManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,29 @@ uint8_t SnesControlManager::Read(uint16_t addr, bool forAutoRead)
return value;
}

void SnesControlManager::Write(uint16_t addr, uint8_t value, bool forAutoRead)
void SnesControlManager::Write(uint16_t addr, uint8_t value)
{
if(!forAutoRead) {
//OUT0 (the pin the controller ports have) is generated by the combination
//of the last value written to 4016.0 by the CPU ORed with the strobe value
//the auto-read circuit sets. If either one is set, OUT0 will be set.
if(_lastWriteValue != value) {
_lastWriteValue = value;
for(shared_ptr<BaseControlDevice>& device : _controlDevices) {
device->WriteRam(addr, value | (uint8_t)_autoReadStrobe);
}
}
}

for(shared_ptr<BaseControlDevice> &device : _controlDevices) {
device->WriteRam(addr, value);
void SnesControlManager::SetAutoReadStrobe(bool strobe)
{
//OUT0 (the pin the controller ports have) is generated by the combination
//of the last value written to 4016.0 by the CPU ORed with the strobe value
//the auto-read circuit sets. If either one is set, OUT0 will be set.
if(_autoReadStrobe != strobe) {
_autoReadStrobe = strobe;
for(shared_ptr<BaseControlDevice>& device : _controlDevices) {
device->WriteRam(0x4016, _lastWriteValue | (uint8_t)strobe);
}
}
}

Expand All @@ -124,4 +139,5 @@ void SnesControlManager::Serialize(Serializer &s)
}

SV(_lastWriteValue);
SV(_autoReadStrobe);
}
5 changes: 4 additions & 1 deletion Core/SNES/SnesControlManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SnesControlManager final : public BaseControlManager
protected:
SnesConsole* _console = nullptr;
uint8_t _lastWriteValue = 0;
bool _autoReadStrobe = 0;

public:
SnesControlManager(SnesConsole* console);
Expand All @@ -36,7 +37,9 @@ class SnesControlManager final : public BaseControlManager
shared_ptr<BaseControlDevice> CreateControllerDevice(ControllerType type, uint8_t port) override;

uint8_t Read(uint16_t addr, bool forAutoRead = false);
void Write(uint16_t addr, uint8_t value, bool forAutoRead = false);

void Write(uint16_t addr, uint8_t value);
void SetAutoReadStrobe(bool strobe);

void Serialize(Serializer &s) override;
};

0 comments on commit 6b77200

Please sign in to comment.