Can I start?
Yes.
Okay.
So, hello everyone.
My name is Alex and I'll be talking today about the chosen horrors of NES Dynamics Recompletion.
I'm basically going to be talking about a project that I did a while ago when I was trying to learn about dynamic
simulation and I wanted to just.
Just a word.
If people still staying here, can you just defrag?
Oh yeah.
So people can take a sit.
Thank you.
Okay, so like I was saying, basically this is a project that I did.
I'll wait.
I'll wait a bit.
Okay, well, like I was saying, basically this is a project that I did while I was trying to learn how
emulators and dynamic reconpiler work.
And I'll be talking a bit about it.
Okay, so first of all, what is an emulator?
I mean, different people have different descriptions about what an emulator is.
But the way that I like to think about it, it's basically you're writing a program that's
faking being another system.
It's tricking the original software into thinking that it's running on the hardware.
It was supposed to run.
So like some popular emulators that are out there are QMU, which allows you basically to run
an version of an operating system on any machine.
You have console emulators like RPCS3, which is a PS3 emulator.
You have V8, which wouldn't you wouldn't normally think of as an emulator.
But it actually compiles your code into an intermediate representation and then compiles
it back to machine code.
So it is actually like an emulator.
And why?
Which unlike what they would like you to think is an emulator.
Of when those furlinics.
Okay, so before you consider writing a reconpiler, you probably should consider writing
an interpreter because an interpreter is actually a lot easier to implement.
You just need to go instruction by instruction of the original software and execute whatever
operations it did.
You don't have to do anything fancy about it.
It's very flexible.
It allows you to avoid a lot of pitfalls with memory and caches and everything.
And it's also very easy to debug.
The con of an interpreter and the basically the only reason you would write anything other than an
interpreter is because interpreting is actually quite slow.
Interpreting a single instruction of the original machine could take like hundreds or
thousands of the host machine.
For the NES, that isn't actually a problem.
The NES has a spew that for today's standards is so slow that it doesn't really matter.
You wouldn't notice, but you've got to learn, you've got to start from somewhere.
So yeah.
And when you move on to write a reconpiler, you basically have two options.
You have a just in time or dynamic reconpiler, which essentially, when you want to execute an instruction,
that is at some address on the original software.
What you do is recompile a bunch of instructions.
You recompile the instruction you want to execute and the one below and the one below that.
And you go on and on until you have to stop for reasons we'll discuss later.
So the main benefits of this is that it's usually quite a lot faster than interpreters.
Because once you have recompiled a code, a block of code, you don't have to recompile it again.
You can use the result from the other one.
So basically, the only time a just in time reconpiler is actually slow is when it encounters a lot of new code.
For example, if you go to a new level on a game or you just start running the game,
that's the only part where it's just time reconpiler would be going a bit slow.
And it also has the benefit that it can perform hot path optimizations.
You can analyze which parts of the code you are running a lot and which ones you're running, you're not running that much.
And say, okay, since this part of the code is actually been run a lot,
I'm going to take a bit of time to optimize it so the next time I run it, it's faster.
But if I have a part of the code that it's only executed once, then there's no need to optimize it.
I could just run it and forget about it.
Yeah.
The major cause of the just in time reconpiler is that it's very hard to debug in real time,
because the program is essentially generating new code to execute for himself.
So you can't really break point on it and see what's going wrong.
You just need to assume what's going wrong based on what's happening.
And the step of reconpiling new code is like an overhead, it takes time.
So if you are trying to emulate a program that it's moving around a lot and it constantly finds new code to execute that could be a problem.
Now the other way to do reconpilation is what's called a head of time or static reconpilation,
which is basically everything you would aspire for a reconpiler.
It just reads the whole original code and then translate it into code for your machine all at once and you're done.
This is obviously the fastest way to emulate a program because there's no need to in the middle of execution.
Of execution do anything different.
You just run a program like it was a regular program for your computer.
And another benefit is that you can reconpile once and then execute the same reconpiled program any time.
You don't have to do reconpilation over and over again.
And it's actually easier to debug than a just in time reconpiler because you're not generating code on the fly.
You're generating code once and so if you want to put a breakpoint at some place it's actually easy to do.
The bad part of the head of time reconpilers is that they're a pain in the ass.
You need to analyze the whole original code find which parts of the code jump to other parts of the code and that's not always obvious.
For example, when you want to call a function pointer, how would you know where that function pointer is in memory.
If I only have a function that it's called by a pointer, how do I find where it is?
If no one's direct recalling it.
So you have to do a lot of analyzing the code and finding where everything is to do that.
And it also cannot do how path of optimizations because you reconpile once.
You don't have the chance to analyze which part of the code is being run more than others.
So this is the basic just about emulation reconpilers.
And my project specifically is an NES dynamic reconpiler.
So I'm no need to analyze the whole program.
It's written in C++20.
It uses L of EM to perform the compilation back to host machine code.
Basically, I recompile NES instructions into L of EM and then L of EM compiles that code into X86 or whatever it is you're running.
And for Windows and event handling, I just use this deal basically.
There we go.
So because this was a small project that I wanted to write,
I said myself the clear mindset that I didn't want to do like the holy grail of emulators.
I wanted to do something that worked and to see.
And so that I could understand the process of how it works.
So some of the things that my emulator cannot do, for example, is it cannot handle self-modifying code.
Like for example, Super Mario Brothers 1 has parts of the code that writes to memory and then execute that memory that it has just written.
That wouldn't be handled on my emulator.
It doesn't have support for all these functions.
I basically just support for now instructions for the games I tested.
So that way it could iterate faster and see where the bugs were and all of that.
The PPU is actually the picture processing unit, which is the one in charge of the graphics.
It's an interpreted that I took from somewhere else because recompiling the PPU code would be,
I mean, not impossible, but it would be headscratcher.
And I also have no support for audio for the time because my focus was on the PPU, on the PPU recompilation.
But it can run backmen.
And if I have time, if I have a bit of time later, I'll show it live, but I put there a gift.
So in case I don't have time, you can see that it actually works.
Thank you.
Okay, so now a bit more into how the emulator works.
So basically, whenever you need to run an instruction, you haven't seen before, a code block that you haven't seen before.
You create on the host machine a function.
And for that function, you basically go iterating, like I said, instruction by instruction.
You go fetching instructions until you reach a point where you stop recompiling instructions.
In this case, one of the ways you can stop a code block from recompiling is saying,
that, for example, you want code blocks to have a maximum of 10,
10 original instructions.
You could, for example, want to limit the size of the original of the code blocks.
So they don't get too big and you're missing parts of the code in the middle that you also want to recompile.
Okay, so when, now we know that code blocks are implemented as host as the host machines functions.
And so whenever we jump from one code block to another, we need to know the state in which the CPU was.
When we executed the last one, now we're entering this one.
So the way we're doing it here is basically, we write the registers and the flag values into memory.
And basically, to each function that acts like a recompile code block, we pass the state of the CPU as a pointer.
So that way we can access all the registers, all the flags, and also the integrated RAM is included in there.
And not technically the CPU, but it makes it easier.
And for regular instructions, like addition, shifting, and all of that, we essentially just take the operations that that instruction did.
And we will translate them. So for in this example, we have the rotate right instruction, which takes a value from memory and then shifts it to the right.
And the right most bid goes to the carrier flag. So we do that. We update some flags.
For conditional branching instructions, for example, for an if.
We tag if the condition we're testing is true. If the condition we're testing is true, we return from the function.
And if it's not true, we keep recompiling the instructions below.
So in the case that we do not take that jump, we don't have to go to a new recompile code block.
And for jams and calls, that mean that we'll always be jumping at that point.
And we will not be executing the instruction below, which has returned from the function.
And from the function, we return the address that we'll want to execute the next.
Okay, I don't have time to explain all the horrors of recompiling the NES.
I'll just leave one here. So in the NES, things like the PPU and the APU perform actions.
Once every certain amount of cycles.
And so what this means for my recompiler is that every time I recompile an instruction, I need to.
Check how many cycles have passed on the CPU.
And then after a certain amount of cycles have passed, I need to jump to an interrupt handler.
Basically, I need to jump to completely different part of the code and execute that instead of the next instruction.
So you have to do that every time you recompile an instruction, which makes it a lot harder for LVM to optimize the recompile code.
And that is it.
Okay.
Well, questions, what do I have to do?
Yeah, I can do the demo.
If anyone has any questions, keep an answer.
Any questions?
We have two minutes.
Okay.
Yeah.
First of all, then I come back.
You've time to lose.
First, let me say, I think it's a great work and a great presentation.
Just let me add one bit.
You wrote that fully static ahead of time compilation has no way to do
Part-path optimization.
Actually, it is possible if you use profile guided optimization.
Yeah.
I mean, it is possible if, but it would be extremely, yeah, it is possible.
But it's not, or this takes a lot.
Yeah.
Yes.
Exactly.
Complicated, but possible.
There was a question over here.
That is a problem.
I don't know if it's.
Did you try any other just-in-time libraries?
Do you have a particular preference for LVM simplmentation?
No.
I don't know if.
I basically did LVM because it's the one I'm most used to.
I haven't checked anyone.
Any other one out?
I basically just used to, uh, LVM for that.
Yeah, yeah, go ahead.
My cable doesn't feed here, so I cannot plug the controller.
Yeah.
So, I'd actually still have one question here.
Would you have a couple of minutes, huh?
I just wanted to know, uh, you said you've tested it with a handful of games.
Uh, what's the list?
Just not a curiosity.
Well, I, first I wanted to try outside bike, but outside bike.
It run, but it had, it had some bug that I wasn't able to figure out what was going wrong.
It's like the bike was going faster than the screen was rolling.
So, it was weird.
I tried Pac-Man because it's one of the simple ones and it did work.
I also tried Donkey Kong, and Donkey Kong also worked.
So yeah, those are the main ones I tested.
Very last, hopefully quick question here.
Hi.
Um, if I end up to it correctly, you said that it was difficult to handle
Seth modifying programs, but I was wondering why because if you're just compiling them,
they should be fine at runtime.
They should be able to modify their stuff, like, like the memory rights.
Yeah.
So the problem with self-modifying code is that whenever you are
Recompiling, when you, when you recompile a code block, you start that, uh,
recompiled, uh, actual machine code for the host, for the host machine, you save it.
But for self-modifying code, every time you write to memory, you need to check
all the code you have already recompiled and say, okay, have I written to a memory
address that I had cashed?
And you have to check that all the time, every time you do, uh, a memory
right.
So it is possible, but it, it slows down the program a lot, and you have to do it right
to know when you invalidate memory addresses one not.
Yeah, that is it.
Many thanks, Alex.
Now it's time for Michelle.
Thank you.