Skip to content

Instantly share code, notes, and snippets.

@CrystalGamma
Last active September 6, 2018 08:10
Show Gist options
  • Save CrystalGamma/d2bb75bd5f248d2e12ab0531d3c00f2b to your computer and use it in GitHub Desktop.
Save CrystalGamma/d2bb75bd5f248d2e12ab0531d3c00f2b to your computer and use it in GitHub Desktop.
Removing the global state from Dolphin's CPU emulation

Removing the global state from Dolphin's CPU emulation

Rationale

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.

Design

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 like last_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.

Open Questions

How should the interpreter function prototype look like?

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.

How to handle SConfig accesses?

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment