This post is just what I'm thinking about concerning the emulator at the moment, what I'm currently
implementing, written down and shared in hopes of obtaining another point of view, even if it's just, "hah,
that's silly."
At first this was going to be a statically-recompiling emulator, but then plans changed when it was
observed that the cost did not justify the benefit in comparison to a Dynarec-based CPU core.
IBA is emulating an ARM processor on another ARM, so statically recompiling with GCC in the
middle wasn't worth it: Register allocation and the code in general would not be optimal (did a
test, results didn't look that good), CPSR flags have to be calculated (rather than using the
GP2X's), and it's rather cumbersome for the end-user (run in decompilation mode, exit,
compile, get back in again, rinse, lather, repeat). Since the whole thing is so experimental, one
should not be surprised with the changes. A GCC backend would probably do a better job with
a processor that has more registers than the ARM (PPC maybe?), but being so similar there is no
need for it and it ends up getting in the way.
A dynarec has been built for outputting blocks of code that supports all of the ARM's instructions
except, currently, the opcodes for co-processor communication. The code it outputs can be saved
to the SD so that ROM code does not have to be re-translated... so in a way, this emulator can still
be a static recompiler, it just does ASM->ASM instead of ASM->C->ASM. (If anybody goes
out and says, "I told you so" I'll pull out a large trout and deliver a few slaps before the day ends)
When the emulator starts, the processor attempts to execute whatever is at address 0x08000000,
which resides on the cartridge (GPROM). To know if the instruction at that address has been
previously compiled, a second table is needed. Due to the size of the GPROM (up to 32MB) a
table that keeps track of what's beed compiled and what hasn't needs to be as compact as possible.
Since the worst case is that all 32MB are filled with Thumb code, you could say that every short (16 bits)
needs one bit to indicate previous compilation. So, 32MBytes / 2Bytes = 16M Thumb instructions =
16Mbits are necessary for flagging each instruction = 2MBytes for the GPROM
Compilation Bit Map (GPROMcbm from now on).
Now that there's a table that tells the emulator that it has executed that instruction before, it needs
to know where the translated block is. We can't afford to have a 32bit pointer for each instruction
(it would require 64MB plus the GPROM's 32MB totalling 96MB!) so we'll have to over-write
whatever the GPROM is storing there with a pointer to the code's actual location. In the case of
ARM code, this is not a problem as the instructions are the same size as a pointer... but what about
thumb mode? A pointer can't be stored in 16bits! Well, where you can't store an absolute address, a
relative one will have to do.
Let's say the letters on the left are GBA instructions and the ones on the right are the translated GP2X
equivalent:
Code:
0 A E
1 F
2 G
3 B H
4 C I
5 J
With 16 bits, if a jump to C is necessary, you can't simply store the pointer to I. Therefore, instead of
a pointer, an offset is used. This gives us 65536 instructions that are possible jump targets, which isn't
enough. The solution (?):
Target = (R15 - 0x08000000) + GPROM[ R15 ] + CacheBlockPtr;
When A is compiled, a zero is put in its place, and the GPROMcbm is updated.
Then B is compiled, a (0+2) two is put in its place, and the GPROMcbm is updated again.
Finally C is compiled, (2+0) two is put in its place, and the GPROMcbm... you'll never guess... is
updated yet once more.
The number that is put in place of the old opcode is the number of "extra" instructions needed up till
now. This gives us far more than 65536 instructions due to the fact that most opcodes are translated
with a 1:1 proportion.
Ok, enough of the GPROM, let's move on to the rest...
The GBA has the following address-space regions:
ROM
Internal RAM
External RAM
IORAM
Palette RAM
Video RAM
Tile RAM
GPROM
I am going to assume code can be executed on the ROM, IRAM, ERAM, and GPROM and will
never be executed on the IORAM, Palette RAM, VRAM, or the Tile RAM.
Executable memory that can't be written to (ROM, GPROM) does not need over-write checking, and
so can be optimized and simplified in comparisson to the RAM.
Let's have a look at Interal / External RAM. These two banks are rather small - 32KB and 256KB - so
there's a lot more room to work with, and I'll need it. Since they're writable, the emulator needs to know
if the block of code it has written is valid. This means that for each GBA instruction, there needs to be a
pointer to the equivalent GP2X code and a pointer to the cDynarec object it came from.
The cDynarec class has a Key and CurrKey. When a cache block is compiled, the sum of each GBA
instruction is stored in Key and CurrKey. That way I hope to have a signature of each block. Each
time a STR/STM instruction is executed on an address that contains compiled code, the old value is
subtracted from the CurrKey and the new one is added. When code generated by cDynarec has to
be executed, it checks if Key equals CurrKey, and if it isn't, the CurrKey is searched for in a binary
tree. If it isn't found, a new block has to be generated, and the current one stored in the tree for later.
If it is found, the old block replaces the current one.
.... I hope that makes sense >_<
As for the other RAMs, I'd like to know... is it reasonable to assume that if a GBA instruction accesses
the IORAM (for example) it is unlikely to access another region? Because if it is, I'd like to do the
MMU search once and then make the generated code access that area directly.