Execution of main loop in emulator.

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Execution of main loop in emulator.
by on (#191442)
Hi, I've finally implemented my SPC700 core, but no DSP yet... I've gotten it up to the point where I can bypass the CMP $2140/BNE $FB loop where it checks for the SPC700 ready value without hard coding everything... Basically, the CPU and APU can now sort of "talk to each other" by reading the memory-mapped IO ports.

But I'm trying to wrap my head around how the main CPU and APU loop should execute... Right now I am doing it in a dumb way by executing one instruction of the CPU and APU at a time... Kind of like this:

Code:
while(true) {
    cpu.run();
    apu.run();
}


So basically I am running 1 instruction of the CPU, then handing off to 1 instruction of the APU. I am counting the instruction cycles internally. My wonder is... How am I supposed to actually correctly execute the main system loop?

Is there a set number of instructions, or cycles I should be executing the CPU before executing the APU? I read something about emulator "catch-up" on the NESDev wiki, but I am not sure how to implement it.

Also I am aware that the chips on the SNES do execute in parallel... so I figure that executing each chip one instruction at a time sort of "emulates" this effect on a much faster machine. I am developing on a dual-core Core i7 3.0GHz (2014 MacBook Pro).

I have actually implemented by CPU and APU at the opcode level.. Which basically is a big switch statement that interprets the current opcode byte and increments the program counter registers by the length of the expected instruction.
Re: Execution of main loop in emulator.
by on (#191448)
So, the main issue here is that the CPU and APU don't run at the same rate at all; they don't even use the same clock. For NTSC, everything with the CPU and PPU is based on master cycles with a rate of 21.44727 MHz. So, for instance, a fast CPU cycle is six master cycles (~3.58 MHz), and it takes the PPU four master cycles per pixel as the screen is displayed. However, the APU uses an entirely separate oscillator at 24.576 MHz, and the SPC700 runs at 1/24 of that rate, at 1.024 MHz.

Because of that, there's not really any clean ratio between the CPU and APU rates, which makes timing tricky. (It can even be a little tricky from the perspective of a SNES game developer, not just an emulator writer, because these rates aren't perfect; they can vary slightly from SNES to SNES because of the separate clocks.)

So, simply running one instruction of the CPU and then one of the APU isn't gonna cut it in terms of actual accuracy. That said, I'm not sure what the best way to approach the problem would be, not having much experience writing emulators myself.
Re: Execution of main loop in emulator.
by on (#191450)
The APU clock has a much wider tolerance than the CPU/PPU master clock. Treating the two oscillators as having exactly a 7:8 ratio is within this tolerance and may simplify emulation. This means that for every 21 cycles of the CPU/PPU master clock, you run 24 cycles of the APU master clock, which consists of one S-SMP cycle and two S-DSP cycles.
Re: Execution of main loop in emulator.
by on (#191462)
tepples wrote:
The APU clock has a much wider tolerance than the CPU/PPU master clock. Treating the two oscillators as having exactly a 7:8 ratio is within this tolerance and may simplify emulation. This means that for every 21 cycles of the CPU/PPU master clock, you run 24 cycles of the APU master clock, which consists of one S-SMP cycle and two S-DSP cycles.

Specifically, something like this:

Code:
procedure Run;
begin
repeat
        // emulate an entire field
        repeat State.Step until     State.SNES.PPU.VBLANK;  Screen.Render;  // get the field to the host display as soon as it's done
        repeat State.Step until not State.SNES.PPU.VBLANK;
        Application.ProcessMessages;
until Application.Terminated;
end;


procedure State.Step;  inline;
begin
SNES.CPU.Step;
SNES.APU.Step;
Inc(APU_ExtraStepCounter);
if (APU_ExtraStepCounter = 7) then begin
        APU_ExtraStepCounter := 0;
        SNES.APU.Step;
end;
end;


Alternatively you can call SNES.APU.Step (and SNES.PPU.Step etc.) inside each opcode handler, and then process the result also in the opcode handler. This means that the flow of execution would go

Code:
Run
        → State.Step
                → CPU.Step
                        → CPU opcode handler
                                → State.Yield
                                        APU.Step, CART.Step, PPU.Step, WRAM.Step, ...
                                ← State.Yield
                        ← CPU opcode handler
                ← CPU.Step
        ← State.Step
Run
Re: Execution of main loop in emulator.
by on (#191467)
The nice thing about emulating SNES main CPU and sound CPU is that the CPU's can't throw interrupts at each other. Instead, they can only communicate via port 214xh. That's making the timing/emulation very simple:

All you need is a "run_apu_till_now" function, which is computing how much time has ellapsed on the main CPU, and then runs the sound CPU for the same amount of time.

Essentially, you need to execute that "run_apu_till_now" function ONLY when the main CPU is reading/writing port 214xh (first call the function, and then apply the 214xh read/write).

Apart from that, you should also call "run_apu_till_now" once or then, eg. upon Vblank (to keep generating sound even in periods where the main CPU doesn't access port 214xh).
Re: Execution of main loop in emulator.
by on (#191654)
nocash wrote:
The nice thing about emulating SNES main CPU and sound CPU is that the CPU's can't throw interrupts at each other. Instead, they can only communicate via port 214xh. That's making the timing/emulation very simple:

All you need is a "run_apu_till_now" function, which is computing how much time has ellapsed on the main CPU, and then runs the sound CPU for the same amount of time.

Essentially, you need to execute that "run_apu_till_now" function ONLY when the main CPU is reading/writing port 214xh (first call the function, and then apply the 214xh read/write).

Apart from that, you should also call "run_apu_till_now" once or then, eg. upon Vblank (to keep generating sound even in periods where the main CPU doesn't access port 214xh).


Hm interesting. I never thought of it that way. How do we determine "till now". I assume that's the parameter of the number of nanoseconds in which the CPU was executing before the 214x access? So let's say the CPU was running for 500ns (arbritrary number) before accessing $2140, does that mean we run the apu for 500ns? Sorry if that sounds kind of dumb to ask. I just wanted to confirm. :)
Re: Execution of main loop in emulator.
by on (#191668)
Under my suggestion, "now" is the master clock divided by 21.