Fix SNES auto-joypad polling

anomie's notes indicate that polling begins between cycles 32.5-95.5, but his
notes were slightly off. 32.5-95.5 is referring to the PPU dot clock. If you
multiply by 4 you get the cycle count, 130-382. It's pretty clear that in
256-cycle units, that would need to be 130-384 or you'd have no event when it
lands on cycle 384. Jonas Quinn confirmed that with his testing in any case.

I chose to split the cycle range in half and run things every 128 clocks. So
that reduces us to 32.5-64.0, or 130 to 256 + 258-384. The reason I chose 128
over 256 was so I could emulate the time it takes to perform the joypad
latching. I don't believe you can set the controller latch to 1 and then 0
immediately. It needs time to let the controllers see this happening and then
release the latch, and time to let that take effect. Polling the controllers is
only one operation so it makes sense that happens once every 256 cycles, which
is why I have the (counter&1) test for that.

Think about it this way: we know the polling sequence takes 4224 cycles, and
4224/256 is 16.5. Isn't that a little strange to have half a cycle? But if we
consider the latching is actually twice as fast, then it's 2x128 + 16x256. If we
run on a 128-clock counter, it becomes 2x128 + 16x2x128, or 34 total states. But
now since we don't actually poll on the second half of each 16x2x256 states, we
don't need to bother running the event there, so we can stop at 33 instead of 34
and it's the same thing. And by doing that, it turns our polling duration into
4224 cycles, just like we've observed.

Fixes #61.
This commit is contained in:
asuramaru 2020-10-21 23:31:10 -04:00 committed by Screwtapello
parent 9d262ed113
commit 39c37ec2d1
6 changed files with 43 additions and 93 deletions

View File

@ -120,7 +120,7 @@ private:
bool autoJoypadActive = 0;
bool autoJoypadLatch = 0;
uint autoJoypadCounter = 0;
uint autoJoypadCounter = 33; //state machine; 4224 / 128 = 33 (inactive)
} status;
struct IO {

View File

@ -5,7 +5,7 @@ auto CPU::dmaCounter() const -> uint {
//joypad auto-poll clock divider
auto CPU::joypadCounter() const -> uint {
return counter.cpu & 255;
return counter.cpu & 127;
}
auto CPU::stepOnce() -> void {
@ -105,7 +105,7 @@ auto CPU::scanline() -> void {
status.hdmaSetupPosition = (version == 1 ? 12 + 8 - dmaCounter() : 12 + dmaCounter());
status.hdmaSetupTriggered = false;
status.autoJoypadCounter = 0;
status.autoJoypadCounter = 33; //33 = inactive
}
//DRAM refresh occurs once every scanline
@ -200,75 +200,47 @@ auto CPU::dmaEdge() -> void {
}
}
//called every 256 clocks; see CPU::step()
//called every 128 clocks; see CPU::step()
auto CPU::joypadEdge() -> void {
//fast joypad polling is a hack to work around edge cases not currently emulated in auto-joypad polling.
//below is a list of games that have had input issues over the years.
//Nuke (PD): inputs do not work
//Super Conflict: sends random inputs even with no buttons pressed
//Super Star Wars: Start button auto-unpauses
//Taikyoku Igo - Goliath: start button not acknowledged
//Tatakae Genshijin 2: attract sequence ends early
//Williams Arcade's Greatest Hits: inputs fire on their own; or menu items sometimes skipped
//World Masters Golf: inputs fail to register; or holding D-pad should only move the cursor once, not continuously
//it is not yet confirmed if polling can be stopped early and/or (re)started later
if(!io.autoJoypadPoll) return;
if(configuration.hacks.cpu.fastJoypadPolling) {
//Taikyoku Igo - Goliath
//Williams Arcade's Greatest Hits
//World Masters Golf
if(!status.autoJoypadCounter && vcounter() >= ppu.vdisp()) {
controllerPort1.device->latch(1);
controllerPort2.device->latch(1);
controllerPort1.device->latch(0);
controllerPort2.device->latch(0);
io.joy1 = 0;
io.joy2 = 0;
io.joy3 = 0;
io.joy4 = 0;
for(uint index : range(16)) {
uint2 port0 = controllerPort1.device->data();
uint2 port1 = controllerPort2.device->data();
io.joy1 = io.joy1 << 1 | port0.bit(0);
io.joy2 = io.joy2 << 1 | port1.bit(0);
io.joy3 = io.joy3 << 1 | port0.bit(1);
io.joy4 = io.joy4 << 1 | port1.bit(1);
}
status.autoJoypadCounter = 16;
}
} else {
if(vcounter() >= ppu.vdisp()) {
//cache enable state at first iteration
if(status.autoJoypadCounter == 0) status.autoJoypadLatch = io.autoJoypadPoll;
status.autoJoypadActive = status.autoJoypadCounter <= 15;
if(status.autoJoypadActive && status.autoJoypadLatch) {
if(status.autoJoypadCounter == 0) {
controllerPort1.device->latch(1);
controllerPort2.device->latch(1);
controllerPort1.device->latch(0);
controllerPort2.device->latch(0);
//shift registers are cleared at start of auto joypad polling
io.joy1 = 0;
io.joy2 = 0;
io.joy3 = 0;
io.joy4 = 0;
}
uint2 port0 = controllerPort1.device->data();
uint2 port1 = controllerPort2.device->data();
io.joy1 = io.joy1 << 1 | port0.bit(0);
io.joy2 = io.joy2 << 1 | port1.bit(0);
io.joy3 = io.joy3 << 1 | port0.bit(1);
io.joy4 = io.joy4 << 1 | port1.bit(1);
}
status.autoJoypadCounter++;
}
if(vcounter() == ppu.vdisp() && hcounter() >= 130 && hcounter() <= 256) {
//begin new polling sequence
status.autoJoypadCounter = 0;
}
//stop after polling has been completed for this frame
if(status.autoJoypadCounter >= 33) return;
if(status.autoJoypadCounter == 0) {
//latch controller states on the first polling cycle
controllerPort1.device->latch(1);
controllerPort2.device->latch(1);
}
if(status.autoJoypadCounter == 1) {
//release latch and begin reading on the second cycle
controllerPort1.device->latch(0);
controllerPort2.device->latch(0);
//shift registers are cleared to zero at start of auto-joypad polling
io.joy1 = 0;
io.joy2 = 0;
io.joy3 = 0;
io.joy4 = 0;
}
if(status.autoJoypadCounter >= 2 && !(status.autoJoypadCounter & 1)) {
//sixteen bits are shifted into joy{1-4}, one bit per 256 clocks
uint2 port0 = controllerPort1.device->data();
uint2 port1 = controllerPort2.device->data();
io.joy1 = io.joy1 << 1 | port0.bit(0);
io.joy2 = io.joy2 << 1 | port1.bit(0);
io.joy3 = io.joy3 << 1 | port0.bit(1);
io.joy4 = io.joy4 << 1 | port1.bit(1);
}
status.autoJoypadCounter++;
}

View File

@ -21,7 +21,6 @@ auto Configuration::process(Markup::Node document, bool load) -> void {
bind(text, "Hacks/Entropy", hacks.entropy);
bind(natural, "Hacks/CPU/Overclock", hacks.cpu.overclock);
bind(boolean, "Hacks/CPU/FastMath", hacks.cpu.fastMath);
bind(boolean, "Hacks/CPU/FastJoypadPolling", hacks.cpu.fastJoypadPolling);
bind(boolean, "Hacks/PPU/Fast", hacks.ppu.fast);
bind(boolean, "Hacks/PPU/Deinterlace", hacks.ppu.deinterlace);
bind(natural, "Hacks/PPU/RenderCycle", hacks.ppu.renderCycle);

View File

@ -33,7 +33,6 @@ struct Configuration {
struct CPU {
uint overclock = 100;
bool fastMath = false;
bool fastJoypadPolling = false;
} cpu;
struct PPU {
bool fast = true;

View File

@ -1,6 +1,5 @@
auto Program::hackCompatibility() -> void {
string entropy = settings.emulator.hack.entropy;
bool fastJoypadPolling = false;
bool fastPPU = settings.emulator.hack.ppu.fast;
bool fastPPUNoSpriteLimit = settings.emulator.hack.ppu.noSpriteLimit;
bool fastDSP = settings.emulator.hack.dsp.fast;
@ -10,15 +9,6 @@ auto Program::hackCompatibility() -> void {
auto title = superFamicom.title;
auto region = superFamicom.region;
//sometimes menu options are skipped over in the main menu with cycle-based joypad polling
if(title == "Arcades Greatest Hits") fastJoypadPolling = true;
//the start button doesn't work in this game with cycle-based joypad polling
if(title == "TAIKYOKU-IGO Goliath") fastJoypadPolling = true;
//holding up or down on the menu quickly cycles through options instead of stopping after each button press
if(title == "WORLD MASTERS GOLF") fastJoypadPolling = true;
//relies on mid-scanline rendering techniques
if(title == "AIR STRIKE PATROL" || title == "DESERT FIGHTER") fastPPU = false;
@ -67,7 +57,6 @@ auto Program::hackCompatibility() -> void {
}
emulator->configure("Hacks/Entropy", entropy);
emulator->configure("Hacks/CPU/FastJoypadPolling", fastJoypadPolling);
emulator->configure("Hacks/PPU/Fast", fastPPU);
emulator->configure("Hacks/PPU/NoSpriteLimit", fastPPUNoSpriteLimit);
emulator->configure("Hacks/PPU/RenderCycle", renderCycle);

View File

@ -165,15 +165,6 @@ auto Program::load() -> void {
auto title = superFamicom.title;
auto region = superFamicom.region;
//sometimes menu options are skipped over in the main menu with cycle-based joypad polling
if(title == "Arcades Greatest Hits") emulator->configure("Hacks/CPU/FastJoypadPolling", true);
//the start button doesn't work in this game with cycle-based joypad polling
if(title == "TAIKYOKU-IGO Goliath") emulator->configure("Hacks/CPU/FastJoypadPolling", true);
//holding up or down on the menu quickly cycles through options instead of stopping after each button press
if(title == "WORLD MASTERS GOLF") emulator->configure("Hacks/CPU/FastJoypadPolling", true);
//relies on mid-scanline rendering techniques
if(title == "AIR STRIKE PATROL" || title == "DESERT FIGHTER") emulator->configure("Hacks/PPU/Fast", false);