In reality... the CPU has its own clock running that tries to simulate the PPU's dot counters (I guess so it doesn't need to poll the PPU for IRQs and such), but these counters don't take long dots into account.
I'll just explain how NMI works since I'm still dreading how I'm going to go about doing IRQ...
There are two variables needed, nmi_line and nmi_read. nmi_line is /NMI, and nmi_read is for $4210 reads.
When the CPU VTIME / HTIME = [225/240] / 0.5 (HC=2), it lowers nmi_read.
When you read from $4210, it returns the inverted status of nmi_read, and then it sets nmi_read back to high. HOWEVER, if VTIME == 225/240, and HTIME == 0.5 (meaning you read $4210 at the exact same instant that nmi_read was raised, then nmi_read is not raised. Why? Probably a bus conflict or something. The CPU is probably still sending the signal to set nmi_read high, and so the PPU read doesn't lower it or something. Bah.
nmi_read is NOT raised at the start of a new frame, but it is upon $4200 write with bit 7 clear.
Next variable, nmi_line, or /NMI as we call it now.
nmi_line is set when CPU VTIME / HTIME = [225/240] / 2.5 (HC=6). 4 cycles later than nmi_read. Very important.
nmi_line is lowered immediately, with no regard to $4200 bit 7. Now when you reach the last bus cycle of the opcode, the CPU checks to see if /NMI is low, and if it is, sets it back to high. Now it sees if NMI interrupts are enabled, and if they are, then on the first bus cycle it gets executed.
nmi_line is probably raised at the start of a new frame, I haven't checked yet. That goes with previous knowledge, however.
But clearly you can see how these two cannot possibly be done with just one variable, /NMI.
This does have some impact on other things, too. Take for example lda $4210 with p.m = 0.
$4210 bit 7 will return set when you're at { 225/240, 0.5 }, even though it isn't the last cycle. With the previous method, it wouldn't be set until the last cycle of the opcode (for the $4211 read).
And just to explain the SNES pipeline for the sake of completeness, take lda $4210 with P.M = 1. The pipeline has two stages, the bus stage and work stage.
The work stage is always 1 cycle behind the bus stage for obvious reasons (how do you manipulate data WHILE its being read?).
So we get:
<Note that each B/W block happen at the exact same time...>
[B1] opfetch
[W?] <last work cycle of previous opcode>
[B2] opread
[W1] <nothing to do>
[B3] opread
[W2] <nothing to do>
/NMI and /IRQ lines are tested here...
[B4] memread
[W3] <nothing to do>
/NMI and /IRQ are invoked here if necessary, /NMI takes priority.
[B1] opfetch
[W4] <$4210 result is now available>
So, even the real SNES has to know when its on the last bus cycle of an opcode in order to test /NMI, /IRQ at the right time. It can't do so any later or the results won't match what we see.
So then there's no real reason to emulate the pipeline, just add a last_cycle() function to the final cycle of every single opcode. It's easier than it sounds, there's never more than two possible last cycles per opcode.
By doing so, you instantly support anomie's chaos IRQ tests. Like:
Code: Select all
phk
ldx.w #next : phx
cli
php
sei
;set IRQ
wai
cli
rti
next:
;no IRQ occured!
I'm quite certain the same holds true for /IRQ, but I guess I'll have to emulate that behavior first, won't I?
What I can say of it right now is that this explains the difference in results for me and anomie:
yx trigger point
00 => Never
01 => H-IRQ: every scanline, H=HTIME+~3.5
10 => V-IRQ: V=VTIME, H=~2.5
11 => HV-IRQ: V=VTIME, H=HTIME+~3.5
Whereas I was using HTIME+4.5 (18) and 3.5 (14).
That's when the /IRQ pin is raised, but reading $4211, as anomie was obviously doing, will be HTIME+3.5 (14), and 2.5 (10).
EDIT: Ok, finished implementing this. It would appear that $4211 read is set at the same time as /IRQ goes low... unlike NMI. So then:
H = (4200 & 0x10) ? (HTIME) : 0;
H = (H != 0) ? ((H << 2) + 18) : 14;
At least, the results are a lot closer this way... still working on it though...