It's written very small, so I'm just going to watch there, I guess.
Okay, more or less, welcome, everybody.
My name is Paul Stackley.
Can you guys hear me on the back?
I guess, how many people saw my talk yesterday?
Oh, okay, quite a few, okay, nice.
So you guys, we have the real picture.
I'm going to give you a talk about how to write a diner X step-by-step.
A little disclaimer, I'm not going to explain how to write your diner X step-by-step.
I'm going to explain how I did write mine.
What's a diner X?
It's kind of a long in a medicine scene.
It stands for dynamic reconpiler.
It's basically a jit compiler, just in time compiler, for binary translation.
To understand what the diner X is, you need to understand what an interpreter is.
An interpreter will read instruction by instruction and simulate them.
On the other hand, dynamic reconpiler will take a chunk of instructions and simulate that
chunk of instructions by generating code for the host CPU.
The reason why we want diner X is they're just fast.
I'm not talking 50% fast, I'm talking 10 times faster.
My own diner X is called life-wreck.
You can click on that thing and give it a start.
Thank you.
It's a mix to everything diner X for PlayStation, and later, notably PCSX.
I started working on it in 2014 as a site project.
The reason was why I decided to work on it was because for me, diner X was just
weird, just black magic.
I just didn't know how they worked.
I didn't understand how they worked.
I'm the best way to understand something, to just do it.
I wanted to do another diner X, but with something that brings something to the table.
The thing I bring, it's a cross-platform diner X.
If you can imagine a CPU that's not M68K, it can run on it.
It uses good lighting as the code generator, and that's how it achieves the cross-platform
ness.
Good lighting is the code generator I was talking about yesterday.
That supports all kinds of architectures.
It runs on PC, you probably used it already, without knowing it, it's the diner X.
In the PCSX, Libertural Core, it runs on GameFube, it runs on Dreamcasts, it runs on other platforms,
on Wii U, etc.
Before even considering writing a diner X, you need to have an interpreter.
For various reasons, the first one being, it's important to emulate the rest of the system
of having a system that works with the interpreter before trying to go into writing a diner X.
It also provides a baseline that will allow you to compare the behavior of the diner X versus
something that you know is working correctly.
Having an interpreter will allow us, or me, to introduce the diner X, by little, and I'm
going to talk a bit more on that later.
In my own diner X, it's complementary to the diner X, basically anything, any piece of code
that is just too hard to translate, I just don't, and I just run the interpreter on it.
I'm going to introduce a few concepts, the first one being a block is a set of consecutive
emulated instructions from the entry points, which I call also PC address, PC address
will be an entry point of a block.
An entry point is point sense if you have a gem to give an address, that given addresses
the entry point of a block, and it ends at an unconditional exit point, for instance, if
you have a gem or a branch somewhere else, that is not conditional, that's an unconditional
exit point, and I'm stopping the block there.
The reason why I'm creating blocks like that is that at compilation time, you just have
no clue about what the code is going to do, for instance, if somewhere in your block you
have an unconditional branch that to give an address that uses the address stored in a
register, you need to know the value of that register, maybe you don't know the value
of that register at that point, so you don't know where it is branching.
You can't just connect this block to another block or just because you don't know where
it is yet.
You can't do static binary translation, many people tell me, why don't you just statically
compile a playstation game for PC, that's just not possible, nobody does that.
We need a block cache, and that's basically just a hash map of the blocks from their
PC address.
So we can easily retrieve the blocks, and the next thing we need is a dispatcher.
The dispatcher, the name I gave it for it, I don't know if there's another term for it,
is the main loop of the dynamic, what it's going to do is fetch the next block from its
PC address, run the interpreter on it or execute it, get the address of the next PC address
as written from the block, and then continue the process, until it's interrupted by an
IRQ of the emulated system, until the time allocated for the CPU emulation is up.
The other role of the dispatcher is to switch between what I call the emulator space and
the gitspace.
Gitspace would be the space of all generative code, and emulator space is everything, as is
all the C code of the emulator, and everything.
The thing is, in C you have rules, for instance, you're not allowed to modify the value of
a register of a colleague, colleague-saved register, there are a few registers, you just
can't modify, and if you modify them in your function, you need to restore the old value.
Yeah, that's the emulator space, in gitspace, you can whatever you do, whatever you want to do.
So what the dispatcher does is save the execution context before restore it afterwards.
The first real thing in it to implement is a call to, I mean, the first thing I implemented,
it was a call to the interpreter, basically you want to generate, you want to implement
generators for all the obstacles of the machine, right?
So what you can do is create a generator for call to the interpreter, that we just interpret
one instruction and then return, and if you use that generator for all the
obstacles of the machine, you already have a working diner wreck, just slow because it's
calling into the interpreter every time, but it's already working, so since it's already working,
the next step is to add tooling because trust me, debugging a diner wreck without tools is really
not fun. For that, I wrote my own debugger, which is called the big as debugger, it's called
the big as debugger because it's really not smart in the way it works, it just runs an instance
of the emulator using the interpreter and an instance of the emulator using the diner wreck and
just compares the two of them at every time the dispatcher loops exit, just going to run
checksums and compare them. I have a very, very debug mode which runs the dispatcher just for
one block and then exit checksum the whole two megabytes of RAM, all the registers are
everything and then run for another block and this one runs in minutes per frame, but it allows
me if there is a breakage somewhere, there's a mis compilation, it allows me to find the exact
block that contains the invalid code. Once you have the tooling and everything, you can start
implementing some opcode, so you generate instruction that will load from the register cache,
the values that you need, that you will generate an instruction that corresponds to the
instruction that you want to emulate for instance a nudge for an add, etc. And then an instruction,
sorry, to save the result back into the register cache, you can keep what I do is I keep a
pointer to my state structure that contains the register cache into a host register for easy access.
One very important thing in a diner, I can very hard to emulate is a code invalidation.
We need a way to invalidate code, for instance, the emality program might want to load
new data from the room or from a disk and write it to somewhere where we thought we had code
and all the blocks we created for that old code, we need to invalidate them. The very simple
solution would be to check the block cache at every memory right, we do an invalidate the block
that we hit. That's very slow, don't do that. The best option, you memory protect the whole run,
using the smallest pages you can, every time you jump to a new PC address, you mark the memory
page as read only. And if you write to read only page then you get an exception, you can switch
to memory page to read right and invalidate all the blocks that are there. It was really well,
it doesn't really work for me because it's tricky to implement for cross platform dynamics.
So what I did was I have a code lookup table, which is the size, it's two megabytes,
the size of my emulated RAM. And basically it's a map between a PC address and the host code
for the block that corresponds at that PC address. So it's a very fast map and it allows me to
do coding validation as well. The memory writes, the generated code that will write a memory
just needs to write a null to the corresponding entry. The dispatcher when it tries to fetch
the next block for that PC address is going to check the loot entry. If it's null then it gets
the block from cache. If there is no block in the cache then it just creates anyone just compiles
on you one. If there is a block however it's going to compute a checksum of the memory that
this block covers. And if the checksum matches the one that was computed at compilation time
of the block then it just resets the loot entry. It's like a force positive resets the null entry.
So that next time we won't have to enter that loop again. Otherwise if it's not matching then it
just regenerates the block. It tries to block and compiles on you one basically. It's not perfect
because for instance if you have a right to offset zero and then jump to sorry a right to offset two
and then jump to offset zero you end up with a block that doesn't really correspond to what is
really in RAM. In practice it works good enough for me at least. The register allocator
it allows you to use host registers for emulated registers because I was saying before
load a register from the register cache emits your opcode then start it again that's low. Don't do that.
If you have a register allocator you can allocate host registers for emulated registers
and try to make it try to spill as many registers sorry spill as less registers as possible
keeping as many emulated registers in host register as possible. I have a multi-level priority system.
I want to flush these to invalidate basically the host registers that are the easiest to
recreate in case I need to recreate them. A good register allocator is really important if you want to
have speed. A little bit about cycle like counting we need to keep track of how many cycles
the way I do it as just keep cycle counter in a host register and increment it on blocks exit
depending on the exit point increment by your different value. You could ask how many cycles
is a memory load. It really depends what memory targets. The PCS6 and Lighttrack they have a very
smart way of delegated it's just to the answer is two instructions sorry two cycles per instruction.
In practice for PlayStation it works mostly okay. Emulating memory that's a hard part and that's
maybe where there is the most to gain with a dynamic. It is hard because in PlayStation
for instance you have the ram at a zero you have several neurons and you have the bios at
the different address and you have the scratch pad at the different address. If you want to emulate
these you have one buffer like you can see for the ram you have one buffer for the bios one
buffer for the scratch pad and you need to map the address between themselves. So the generated
code can become really really big. One simple solution is to generate to have a fast pass
under slow pass. So you generate an address check in the generated code that will check a
isn't this address in ram if it's in ram then you go to a fast pass where it's just loading
doing the address translation and load from ram otherwise it just jumped to C. When I call the
yellow solution is just assume everything points to ram and then you get a segmentation fault you
handle the segmentation fault and continue. My solution I just unconditionally jump to C
then in the C code I check if it's pointing if the address is pointing to a memory or
hardware IO and then I tag the output in the block and recompile the block then in the block
when it's recompiled if it's points to memory then I generate a very fast code for that
otherwise I just jump to C. It works just because people don't try to break it because it
it's based on the assumption that something that points to memory will always point to memory
and something that points to memory IO will always point to memory IO.
Memory translation so I was saying that you need to translate between
emulated memories to host memories you can help this a lot if you memory map the
emulated buffers to where they're actually on the emulated system so in my case I memory
memory map the emulated ram the buffer for the emulated ram to address zero where the
page station exists expected and that means that my address translation is much shorter
and it's very important to make it much shorter because smaller code is faster code.
Finally, a miss optimization that's like the last 10% of the performance these are the
costly ones you count maybe one year for the first person then two years for the next and for
for et cetera so you have for instance red it compilers you can run the compilers one
person red constant folding is you analyze the code see where the constants or if you
addition I add each addition it if you add a constant with a constant it just makes another
constant memory target deduction if you have a constant you know it's it points to ram and
then you load let's say 16 bit or 8 bit value from ram and you add the two values you don't know
exactly what the address is right but you know it's points to ram so you can actually emit
fast pass for that for that memory IO. Instruction sorry combining just you see that
this instruction you could do then better with a host instruction that does the two in one
and define behavior optimization the PlayStation has this break of the code and the break
what the break of code does is just crash the PlayStation so if you if you find it in the
emulated code you know that everything in that branch is dead so you can remove the whole branch
times up yeah local branch is just instead of going back to the dispatcher every time you are
on the branch so you handle the block branching inside itself and target specific optimizations
in my case that's inside new lightning for instance for the SH4 is just moving the instruction so
that they combine together for pipeline stuff like that yeah that was as good as I could do in 20 minutes
thank you
no
yeah the question is are any of these optimizations part of new lightning more or less right
so a new lightning does a little bit of optimization but just a time if it finds
and see if you set a register 0 to 0 and then you set register 0 to 0 is going to sing her
I could skip one that's about the level of optimizations that lighting does everything else is done in
light break and on top of it yeah so if you have forward job the question is if you have a forward
jump do I need a label yes the question is if I have a forward jump I need to resolve the
address before before I can continue with the block right yeah so there are two cases first case is
it's not local then I just jump back to the dispatcher if it's local then I memorize the address
of the branch and later when I know exactly where I'm branching to I'm going to patch that address
that's that's offered by a new lightning by the code editor yeah the generator
somewhat will be more occurring than others so you don't really need to translate everything
because some instructions might appear once every 100 times so in your opinion in your experience
what is the amount of instructions that you need to dynamic in order to gain and ask these and speed
up so you cannot so you don't have to translate everything so the question is
I'm trying to think of the sentence sorry the question is what instructions you absolutely have
to generate code for and in in yeah memory are you that's the most important one yeah
because it's the these instructions we generate maybe the most code but you need them to be as
small as possible to to get good performance yeah
do you always have to start awkward by awkward before moving to instruction combining
just for example my space there's some of course not like loading data in between register
not really anything I would assume those will start by doing the combined work first
can you repeat the question and I'm sorry how many instructions might be in the other console
yeah they don't really do much by themselves they like know data in the special weather yeah
like I don't feel like that there's like how do you start by doing not first and then
finding it later the question is how do I how to replace that how do I select
how do I implement an upcode first before doing the whole part that's so in my slide four I think
I have this code to interpret it basically at that point every upcode just jumps to the
interpreter right and then I can one by one rewrite the code generators to admit better code
that doesn't jump to the interpreter to generate you know I don't see for example they I have
a similar instruction for dividing for floats at the same time if I just do the loading
instruction in diner I think the rest is in there but I don't see our work you don't that's the thing
no no you have to do the whole block the whole block in diner or the whole block in interpreter yeah
we said that the block that starts with an entry point and as we have an additional thing
what I'm going to do if they call the jams into the middle of the block you just generate the
I if so the question is what why do I what do I do if the code writes itself in the middle of
the block or if I jump in the middle of the block I never jump in the middle of the block
because that's a new block that's the short version if you have half an hour we can do the
long version but I have a plane to catch
the question is did I meet some cases where the code is just way too big
to weird oh I've seen weird code believe me I've seen things you people would understand
do you guys know meps assembly what happens when you have a branch as a daily slot of a branch
for question sorry it's going to fetch like that it's going to execute the first instruction at the
address of the target yeah the target of the first branch and then jump to the address of the second
branch what happens if the first instruction at the target of the first branch is a branch
yeah it gets deep like that that's the kind of blocks I was talking at the beginning where I just
jump to the interpreter because that's just impossible to handle in diner or yeah
yeah