2. Emulation Core - Planet Virtual Boy Emulator
The emulation core is a self-contained library module written in the C programming language. C was selected because of its widespread availability and intrinsic efficiency: things expressed in C code are generally converted into machine code with minimal modification, allowing the programmer to decide with significant granularity how the software operates on the hardware. Although it requires immaculate attention to detail in order to prevent problems from occuring, C does provide one of the most versatile solutions to low-level software development.
In order to facilitate portability, the emulation core module is written in pure C code: it does not rely on any external libraries or even the standard runtime. With no reliance on implementation dependent features, the module will operate consistently wherever it is used. With no reliance on the standard runtime, the module's memory footprint and overhead are kept to a minimum. The trade-off to portability is that all I/O, synchronization and timing is the responsibility of the encapsulating application. For instance, it's the application's job to supply controller input to the core, and to present audio/video output to the user.
In its basic state, the emulation core only manages the state of a simulated Virtual Boy. The simulated CPU, hardware components and memory map are managed in a system state construct, but no other operations are performed. Additional features such as savestates, cheat codes or runtime breakpoints are beyond the scope of the emulation core and must be implemented by the encapsulating application instead.
The top-level data element of the emulation core is the state context, which is a struct containing all fields necessary for managing the state of the simulated Virtual Boy. ROM and SRAM buffers need to be supplied by the encapsulating application, but otherwise all state information is defined within the context struct. Most of the emulation core API functions operate on an instance of a state context.
Most of the emulation core's functionality is handled through the Emulate command, which continuously manages simulation operations until some break condition occurs. A break condition can be determined by the encapsulating application in response to things such as memory accesses or interrupts, but will otherwise occur automatically after a preset number of simulated CPU cycles as determined by the application.
Synchronization between the simulated state context and user input/output is the responsibility of the encapsulating application. This can be managed with break conditions on video interrupts and CPU cycle counts.
During the processing of an Emulate command, certain key points in the simulation routines can optionally call functions that belong to the encapsulating application. This is necessary in order to prevent the Emulate command from executing indefinitely, but can also be used to great effect by the application to implement any number of auxiliary features that are not built into the emulation core. Using callback functions necessarily increases the overhead associated with the simulation, so care should be taken to make them as succinct as possible.
All callback functions provided by the encapsulating application return an application-defined break code, or zero if no break is to occur. If an application break is requested, simulation processing immediately aborts and the break code is propagated all the way back up the call stack, becoming the return value of the Emulate command.
Callback hooks are present in the emulation core for each of the following events:
|• Read||Bus load or input|
|• Write||Bus store or output|
|• Execute||Intruction processing|
|• Exception||Exception, interrupt or hardware breakpoint processing|
These callbacks apply to the general case. For example, the execute callback is called for every instruction regardless of its address. It is the responsibility of the encapsulating application to check the state context when deciding how to process events within callback functions.
Read and write callbacks are called prior to performing the corresponding bus operation. This occurs when fetching instruction bits as well as during the processing of certain instructions. A struct will be passed to the callback function containing information about the desired access operation, such as its address and data type. The emulation core will not automatically perform bus operations if a corresponding callback is provided: the callback itself must perform bus operations using an API call. This enables the callback to handle accesses either before or after they occur.
The execute callback is called prior to processing an instruction. A struct will be passed to the callback function containing information about the instruction to execute, such as its opcode and operands. Upon returning, the emulation core will normally process the instruction in its current state, but the callback is allowed to cancel this processing and skip the instruction entirely.
The exception callback is called prior to processing an exception (interrupts are a subset of exceptions). A struct will be passed to the callback function containing information about the exception, such as its numeric code and whether or not it is an interrupt. Upon returning, the emulation core will normally process the exception in its current state, but the callback is allowed to cancel this processing.
Source Code Conventions
C source code for the emulation core library follows a few general guidelines, but otherwise its design is at the discretion of the programmer:
Only the functions defined in the library API may be exposed to the rest of
the program: all internal functions must be declared
- No implementation dependent assumptions may be made. For instance, some data types have indeterminate size, endianness is not guaranteed and sign extension via the right-shift operator is not guaranteed.
The source code must adhere to features available in the C89/C90 version of
the C spec. This means certain features present in later versions are not
available, including the
//style of comments.
The code must compile in gcc with no warings when using
-std=c90 -Wall -Wext. This will maximize compatibility with other software projects.