This is for a bug I recently came across with Mortal Kombat II. It applies both to NES and SNES, as they have virtually identical underpinnings, so I'm hoping someone here has experimented at this low level of detail and can shed some insight on this matter for me.
This is really deep into clock-perfect timing, so this is not for the inexperienced or faint of heart. You probably won't be able to help unless you really know this stuff inside and out.
Here is my IRQ log:
http://byuu.cinnamonpirate.com/temp/irq.txt
The second block is the problem.
$4200 is the SNES NMI/IRQ enable register. As we know, a write that happens immediately at the same time as the lastcycle test does not get processed on that lastcycle event. It's as if the $4200 register holds its' previous value for the IRQ test (meaning lastcycle is tested first before the write takes event), or the IRQ test is simply blocked for this lastcycle event (eg a simple delay is needed).
What I do currently is implement a tiny delay by 2 clock ticks, to prevent the lastcycle test from succeeding here, because the former would require SNES CPU pipeline emulation to detect lastcycle edge before the previous cycles' write completes. The problem is that I then test to trigger IRQs again two ticks later, and now I see that V=215, and VIRQs are enabled on line 215, so I internally say "ok, we need to generate a VIRQ". cli doesn't occur until V=216, but an IRQ triggers here anyway because once the /IRQ line is raised, it stays raised. Hardware disagrees with me on this specific test. Note that on real hardware, if the lastcycle test were to happen 6 clocks sooner, it really would fire a second IRQ. So yes, this game will actually completely break, even on real hardware, if timing is adjusted by a mere four hertz on a 21mhz clock. And people say cycle-accurate emulation isn't necessary :P
Anyway, I'm kind of at a loss on how to fix this. The reason it sees V=216,HCLOCK=6 as being valid for V=215 IRQ is because of the delay between your IRQ/NMI positions and when they actually trigger. For NMI, the delay is 2 clocks, just like the NES NMI. For SNES IRQs, the delay is 10 clocks for VIRQ, 14 clocks for [V]HIRQ. So, subtract 10 from V216, HCLOCK6, and you get V215,HCLOCK1360. It thinks V==VTIME, so /IRQ gets ticked.
What I'm thinking is happening, is that because the write happened to $4200 right at the lastcycle edge, it isn't acknowledged here, and the lastcycle event doesn't set the flag to say "ok, we need to trigger another IRQ". And indeed, that's what happens with bsnes now. However, on the next clock, which is not a lastcycle edge, bsnes tests again (it tests every clock because range testing is a major PITA), and sees "ok, NOW the IRQ is valid, let's trigger it on the next lastcycle edge". Note that even range testing wouldn't fix this, as in this case if the SNES really were range testing (highly improbable), the range from immediately after the lastcycle edge would be tested and valid, so the IRQ would trigger anyway on the next lastcycle edge.
Is it possible that the real hardware doesn't actually raise the NMI/IRQ pins immediately when the clock hits their positions, and instead only raises them when the IRQ values are within range during lastcycle edges? The only way to verify this for sure would be with an oscilloscope watching the /NMI and /IRQ lines.
If bsnes were to ignore the "IRQ needs to fire" message at that lastcycle edge due to $4200 write at the same time, and then continued to ignore the message until the next lastcycle edge, no IRQ would trigger. Does this seem like the correct fix, or am I perhaps missing something else?
The problem with this theory is that it would screw with the four-clock "line raised to acknowledgement" delay. You know, say with NMI, it gets raised at clock 2, and then after the delay, the NMI won't fire until clock 6. So I would have to test the IRQ every clock tick, and then when the /IRQ line ticks, and the four-clock delay completes, then set another intermediate variable that says "ok we have an IRQ ready, but we need to wait until lastcycle to actually set the flag that says the IRQ has to fire, and give lastcycle a chance to dismiss this flag". Yet another layer of indirection to triggers the interrupts, doesn't sound correct to me either.
This is really deep into clock-perfect timing, so this is not for the inexperienced or faint of heart. You probably won't be able to help unless you really know this stuff inside and out.
Here is my IRQ log:
http://byuu.cinnamonpirate.com/temp/irq.txt
The second block is the problem.
Code:
80f826 stx $4200 [$804200] A:00c2 X:0020 Y:0000 S:01fb D:0000 DB:80 nVMxdIzC V:215 H:1346
- 1346 <opfetch>
- 1352 <$00 fetch>
- 1358 <$42 fetch>
- 0 <$4200 write start>
- 6 <$4200 actual write>
<lastcycle test>
- 6 <$4201 write start>
- 12 <$4201 actual write>
- 12 <opend>
80f829 cli A:00c2 X:0020 Y:0000 S:01fb D:0000 DB:80 nVMxdIzC V:216 H: 12
- 1346 <opfetch>
- 1352 <$00 fetch>
- 1358 <$42 fetch>
- 0 <$4200 write start>
- 6 <$4200 actual write>
<lastcycle test>
- 6 <$4201 write start>
- 12 <$4201 actual write>
- 12 <opend>
80f829 cli A:00c2 X:0020 Y:0000 S:01fb D:0000 DB:80 nVMxdIzC V:216 H: 12
$4200 is the SNES NMI/IRQ enable register. As we know, a write that happens immediately at the same time as the lastcycle test does not get processed on that lastcycle event. It's as if the $4200 register holds its' previous value for the IRQ test (meaning lastcycle is tested first before the write takes event), or the IRQ test is simply blocked for this lastcycle event (eg a simple delay is needed).
What I do currently is implement a tiny delay by 2 clock ticks, to prevent the lastcycle test from succeeding here, because the former would require SNES CPU pipeline emulation to detect lastcycle edge before the previous cycles' write completes. The problem is that I then test to trigger IRQs again two ticks later, and now I see that V=215, and VIRQs are enabled on line 215, so I internally say "ok, we need to generate a VIRQ". cli doesn't occur until V=216, but an IRQ triggers here anyway because once the /IRQ line is raised, it stays raised. Hardware disagrees with me on this specific test. Note that on real hardware, if the lastcycle test were to happen 6 clocks sooner, it really would fire a second IRQ. So yes, this game will actually completely break, even on real hardware, if timing is adjusted by a mere four hertz on a 21mhz clock. And people say cycle-accurate emulation isn't necessary :P
Anyway, I'm kind of at a loss on how to fix this. The reason it sees V=216,HCLOCK=6 as being valid for V=215 IRQ is because of the delay between your IRQ/NMI positions and when they actually trigger. For NMI, the delay is 2 clocks, just like the NES NMI. For SNES IRQs, the delay is 10 clocks for VIRQ, 14 clocks for [V]HIRQ. So, subtract 10 from V216, HCLOCK6, and you get V215,HCLOCK1360. It thinks V==VTIME, so /IRQ gets ticked.
What I'm thinking is happening, is that because the write happened to $4200 right at the lastcycle edge, it isn't acknowledged here, and the lastcycle event doesn't set the flag to say "ok, we need to trigger another IRQ". And indeed, that's what happens with bsnes now. However, on the next clock, which is not a lastcycle edge, bsnes tests again (it tests every clock because range testing is a major PITA), and sees "ok, NOW the IRQ is valid, let's trigger it on the next lastcycle edge". Note that even range testing wouldn't fix this, as in this case if the SNES really were range testing (highly improbable), the range from immediately after the lastcycle edge would be tested and valid, so the IRQ would trigger anyway on the next lastcycle edge.
Is it possible that the real hardware doesn't actually raise the NMI/IRQ pins immediately when the clock hits their positions, and instead only raises them when the IRQ values are within range during lastcycle edges? The only way to verify this for sure would be with an oscilloscope watching the /NMI and /IRQ lines.
If bsnes were to ignore the "IRQ needs to fire" message at that lastcycle edge due to $4200 write at the same time, and then continued to ignore the message until the next lastcycle edge, no IRQ would trigger. Does this seem like the correct fix, or am I perhaps missing something else?
The problem with this theory is that it would screw with the four-clock "line raised to acknowledgement" delay. You know, say with NMI, it gets raised at clock 2, and then after the delay, the NMI won't fire until clock 6. So I would have to test the IRQ every clock tick, and then when the /IRQ line ticks, and the four-clock delay completes, then set another intermediate variable that says "ok we have an IRQ ready, but we need to wait until lastcycle to actually set the flag that says the IRQ has to fire, and give lastcycle a chance to dismiss this flag". Yet another layer of indirection to triggers the interrupts, doesn't sound correct to me either.