Removing global state will make it easier to instantiate a CPU in isolation, which will make creating a test suite for the PPC emulation much simpler.
Following proper object-oriented design (damn I hate that phrase, but I think here it is appropriate) might also have other maintainability benefits.
Most global variable/stateful function accesses, as far as I have seen, fall into these categories:
- Access to ppcState
- Access to Interpreter variables from static interpreter functions (mostly
m_end_block
, but also odds and ends likelast_pc
and the reservation address) - Access to adjecent systems (Audio, MMIO, Gather Pipe, …)
PowerPC.{h,cpp} will be lifted into a class, which owns a PPCState
instance
(maybe rename to just state
if we're making it a member anyway?),
the guest memory arena (the Memory
namespace, lifted into a class) and the currently-active CPU core.
The CPU namespace will be lifted into a class as well, which owns a PowerPC instance and is responsible for interfacing with most adjecent systems.
Most of JitInterface.{h,cpp} should probably be turned into a member of PowerPC
or a virtual member of JitBase
.
Removing global state will require changing the prototype of the interpreter functions
used in Interpreter (duh) and for fallbacks in the JITs. (See Open Questions)
While making this change, it might also be useful to revamp the CPU exception handling to remove certain state altogether
(Interpreter::m_end_block
and most of PPCState::Exceptions
).
Since an instruction can only continue the current block, jump or cause one interrupt, I propose adding an enum like this, which will be returned by every interpreter function:
enum OpResult : u32
{
/// Execution resumes as normal; NPC = PC + 4
Continue = 0,
/// NPC may be different from PC + 4, ends the block on Interpreter
Jump = 1,
/// block has to be ended for other reasons (e. g. breakpoint)
EndBlock,
// the following are analogous to the current exception values:
Syscall,
DSI,
ISI,
Alignment,
FPUUnavailable,
Program,
// possibly one for Memcheck pseudo exceptions? I haven't looked too deeply into that …
// async interrupts:
Decrementer,
PerformanceMonitor,
ExternalInterrupt
};
This will allow the caller to check for exceptions or other block-ending events
by inspecting the return-value register instead of loading from a memory location
and would eliminate synchronous exception state from PPCState
.
Asynchronous interrupts like the Decrementer, performance monitor and external interrupts
should be handled the following way: CheckExternalExceptions
will return an OpResult
for an interrupt that is active and pending and clear the flag bit.
CoreTiming::Advance
updates PPCState::Exceptions
and returns the result of CheckExternalExceptions
.
Instructions that may cause interrupts to be enabled (like rfi
) should tail call CheckExternalExceptions
as well
(if they did not cause an exception themselves).
Like this, PPCState::Exceptions
will only be used by external exceptions (which are true state)
and not for synchronous exceptions (which are events).
The ppcState quick access macros will be changed to assume either a cpu
(reference to CPU
)
or a ppc
(reference to PowerPC
) variable in scope (see Open Questions),
which may be supplied either as a parameter or a class member where required.
Interpreter should be turned stateless. Besides obviating the need for m_end_block
as above,
m_prev_inst
can be turned into a local in Interpreter::SingleStepInner
.
m_reserve
and m_reserve_address
are emulated CPU state and thus belong to PPCState
anyway.
last_pc
could probably be tucked at the end of PPCState
, even if only Interpreter
uses it.
Thus, the Interpreter
class afterwards only exists for its VTable, and can mostly be treated like any other CPU core
(except it doesn't implement JitBase
functionality). This simplifies a lot of further considerations.
Since most MMIO interfaces are global state currently (and making them objects would increase the scope of this change even further),
they should be registered with the memory map by whatever instantiates PowerPC
.
Whether to use Wii EXRAM should be passed in during instantiation as well, since that cannot be changed during emulation.
This is mostly about how to access state in interpreter functions. It has direct ramifications for how a "CPU-in-isolation" (as used for CPU tests) has to be instantiated.
For instance, if interpreter functions are to take a reference to CPU
, that will require every CPU-in-isolation to have an instance of that.
If, however, interpreter functions take a reference to PowerPC
, that may have a nullable pointer to CPU
which can then
be checked by the hopefully rare functions that need access to adjecent systems.
Various places in PowerPC/
use SConfig
to check whether to skip FPRF emulation,
enable debugging functionality or enable/disable various JIT behaviors.
Are these values supposed to be changeable at runtime or could they be set as a member of, e. g. PowerPC
at instantiation?