mirror of
https://github.com/DrBeef/Raze.git
synced 2024-11-16 01:11:28 +00:00
718112a8fe
Currently none of these is being used, but eventually they will, once more code gets ported over. So it's better to have them right away and avoid editing the project file too much, only to revert that later.
194 lines
9 KiB
Text
194 lines
9 KiB
Text
Game_Music_Emu 0.6.0 Design
|
|
---------------------------
|
|
This might be slightly out-of-date at times, but will be a big help in
|
|
understanding the library implementation.
|
|
|
|
|
|
Architecture
|
|
------------
|
|
The library is essentially a bunch of independent game music file
|
|
emulators unified with a common interface.
|
|
|
|
Gme_File and Music_Emu provide a common interface to the emulators. The
|
|
virtual functions are protected rather than public to allow pre- and
|
|
post-processing of arguments and data in one place. This allows the
|
|
emulator classes to assume that everything is set up properly when
|
|
starting a track and playing samples.
|
|
|
|
All file input is done with the Data_Reader interface. Many derived
|
|
classes are present, for the usual disk-based file and block of memory,
|
|
to specialized adaptors for things like reading a subset of data or
|
|
combining a block of memory with a Data_Reader to the remaining data.
|
|
This makes the library much more flexible with regard to the source of
|
|
game music file data. I still added a specialized load_mem() function to
|
|
have the emulator keep a pointer to data already read in memory, for
|
|
those formats whose files can be absolutely huge (GYM, some VGMs). This
|
|
is important if for some reason the caller must load the data ahead of
|
|
time, but doesn't want the emulator needlessly making a copy.
|
|
|
|
Since silence checking and fading are relatively complex, they are kept
|
|
separate from basic file loading and track information, which are
|
|
handled in the base class Gme_File. My original intent was to use
|
|
Gme_File as the common base class for full emulators and track
|
|
information-only readers, but implementing the C interface was much
|
|
simpler if both derived from Music_Emu. User C++ code can still benefit
|
|
from static checking by using Gme_File where only track information will
|
|
be accessed.
|
|
|
|
Each emulator generally has three components: main emulator, CPU
|
|
emulator, and sound chip emulator(s). Each component has minimal
|
|
coupling, so use in a full emulator or stand alone is fairly easy. This
|
|
modularity really helps reduce complexity. Blip_Buffer helps a lot with
|
|
simplifying the APU interfaces and implementation.
|
|
|
|
The "classic" emulators derive from Classic_Emu, which handles
|
|
Blip_Buffer filling and multiple channels. It uses Multi_Buffer for
|
|
output, allowing you to derive a custom buffer that could output each
|
|
voice to a separate sound channel and do different processing on each.
|
|
At some point I'm going to implement a better Effects_Buffer that allows
|
|
individual control of every channel.
|
|
|
|
In implementing the C interface, I wanted a way to specify an emulator
|
|
type that didn't require linking in all the emulators. For each emulator
|
|
type there is a global object with pointers to functions to create the
|
|
emulator or a track information reader. The emulator type is thus a
|
|
pointer to this, which conveniently allows for a NULL value. The user
|
|
referencing this emulator type object is what ultimately links the
|
|
emulator in (unless new Foo_Emu is used in C++, of course). This type
|
|
also serves as a useful substitute for RTTI on older C++ compilers.
|
|
|
|
Addendum: I have since added gme_type_list(), which causes all listed
|
|
emulators to be linked in. To avoid this, I make the list itself
|
|
editable in blargg_config.h. Having a built-in list allows
|
|
gme_load_file() to take a path and give back an emulator with the file
|
|
loaded, which is extremely useful for new users.
|
|
|
|
|
|
Interface conventions
|
|
----------------------
|
|
If a function retains a pointer to or replaces the value of an object
|
|
passed, it takes a pointer so that it will be clear in the caller's
|
|
source code that care is required.
|
|
|
|
Multi-word names have an underscore '_' separator between individual
|
|
words.
|
|
|
|
Functions are named with lowercase words. Functions which perform an
|
|
action with side-effects are named with a verb phrase (i.e. load, move,
|
|
run). Functions which return the value of a piece of state are named
|
|
using a noun phrase (i.e. loaded, moved, running).
|
|
|
|
Classes are named with capitalized words. Only the first letter of an
|
|
acronym is capitalized. Class names are nouns, sometimes suggestive of
|
|
what they do (i.e. File_Scanner).
|
|
|
|
Structure, enumeration, and typedefs to these and built-in types are
|
|
named using lowercase words with a _t suffix.
|
|
|
|
Macros are named with all-uppercase words.
|
|
|
|
Internal names which can't be hidden due to technical reasons have an
|
|
underscore '_' suffix.
|
|
|
|
|
|
Managing Complexity
|
|
-------------------
|
|
Complexity has been a factor in most library decisions. Many features
|
|
have been passed by due to the complexity they would add. Once
|
|
complexity goes past a certain level, it mentally grasping the library
|
|
in its entirety, at which point more defects will occur and be hard to
|
|
find.
|
|
|
|
I chose 16-bit signed samples because it seems to be the most common
|
|
format. Supporting multiple formats would add too much complexity to be
|
|
worth it. Other formats can be obtained via conversion.
|
|
|
|
I've kept interfaces fairly lean, leaving many possible features
|
|
untapped but easy to add if necessary. For example the classic emulators
|
|
could have volume and frequency equalization adjusted separately for
|
|
each channel, since they each have an associated Blip_Synth.
|
|
|
|
Source files of 400 lines or less seem to be the best size to limit
|
|
complexity. In a few cases there is no reasonable way to split longer
|
|
files, or there is benefit from having the source together in one file.
|
|
|
|
|
|
Preventing Bugs
|
|
---------------
|
|
I've done many things to reduce the opportunity for defects. A general
|
|
principle is to write code so that defects will be as visible as
|
|
possible. I've used several techniques to achieve this.
|
|
|
|
I put assertions at key points where defects seem likely or where
|
|
corruption due to a defect is likely to be visible. I've also put
|
|
assertions where violations of the interface are likely. In emulators
|
|
where I am unsure of exact hardware operation in a particular case, I
|
|
output a debug-only message noting that this has occurred; many times I
|
|
haven't implemented a hardware feature because nothing uses it. I've
|
|
made code brittle where there is no clear reason flexibility; code
|
|
written to handle every possibility sacrifices quality and reliability
|
|
to handle vaguely defined situations.
|
|
|
|
|
|
Flexibility through indirection
|
|
-------------------------------
|
|
I've tried to allow the most flexibility of modules by using indirection
|
|
to allow extension by the user. This keeps each module simpler and more
|
|
focused on its unique task.
|
|
|
|
The classic emulators use Multi_Buffer, which potentially allows a
|
|
separate Blip_Buffer for each channel. This keeps emulators free of
|
|
typical code to allow output in mono, stereo, panning, etc.
|
|
|
|
All emulators use a reader object to access file data, allowing it to be
|
|
stored in a regular file, compressed archive, memory, or generated
|
|
on-the-fly. Again, the library can be kept free of the particulars of
|
|
file access and changes required to support new formats.
|
|
|
|
|
|
Emulators in general
|
|
--------------------
|
|
When I wrote the first NES sound emulator, I stored most of the state in
|
|
an emulator-specific format, with significant redundancy. In the
|
|
register write function I decoded everything into named variables. I
|
|
became tired of the verbosity and wanted to more closely model the
|
|
hardware, so I moved to a style of storing the last written value to
|
|
each register, along with as little other state as possible, mostly the
|
|
internal hardware registers. While this involves slightly more
|
|
recalculation, in most cases the emulation code is of comparable size.
|
|
It also makes state save/restore (for use in a full emulator) much
|
|
simpler. Finally, it makes debugging easier since the hardware registers
|
|
used in emulation are obvious.
|
|
|
|
|
|
CPU Cores
|
|
---------
|
|
I've spent lots of time coming up with techniques to optimize the CPU
|
|
cores. Some of the most important: execute multiple instructions during
|
|
an emulation call, keep state in local variables to allow register
|
|
assignment, optimize state representation for most common instructions,
|
|
defer status flag calculation until actually needed, read program code
|
|
directly without a call to the memory read function, always pre-fetch
|
|
the operand byte before decoding instruction, and emulate instructions
|
|
using common blocks of code.
|
|
|
|
I've successfully used Nes_Cpu in a fairly complete NES emulator, and
|
|
I'd like to make all the CPU emulators suitable for use in emulators. It
|
|
seems a waste for them to be used only for the small amount of emulation
|
|
necessary for game music files.
|
|
|
|
I debugged the CPU cores by writing a test shell that ran them in
|
|
parallel with other CPU cores and compared all memory accesses and
|
|
processor states at each step. This provided good value at little cost.
|
|
|
|
The CPU mapping page size is adjustable to allow the best tradeoff
|
|
between memory/cache usage and handler granularity. The interface allows
|
|
code to be somewhat independent of the page size.
|
|
|
|
I optimize program memory accesses to direct reads rather than calls to
|
|
the memory read function. My assumption is that it would be difficult to
|
|
get useful code out of hardware I/O addresses, so no software will
|
|
intentionally execute out of I/O space. Since the page size can be
|
|
changed easily, most program memory mapping schemes can be accommodated.
|
|
This greatly reduces memory access function calls.
|
|
|