mupen64plus / mupen64plus-core

Core module of the Mupen64Plus project
1.32k stars 258 forks source link

Make make_run reentrant #100

Open Narann opened 9 years ago

Narann commented 9 years ago

@twinaphex discussed about making make_run function reentrant here. Here are his words:

One of the big things that still separates Mupen64plus libretro from Mupen64plus upstream is that for Mupen64plus libretro, I took the effort of making sure main_run is nonblocking and can be called in a reentrant fashion. I need this not only for the purpose of being able to one-shot per frame in the main loop, but also so that I can do realtime rewind (since the savestate code also had to be made reentrant).

In that regard, things are still not really satisfactory right now in the Libretro fork since I still need libco to be able to skip between threads cooperatively (the main thread is for libretro, the other thread is for Mupen64plus). I'd like that to not be needed so that instead we will have a model for the Mupen64plus emulation core that is much more like a conventional emulator like SNES9x or FCEU, one where there are separate global functions for initing, deiniting, and iterating for one frame run.

@Nebuleon's answer:

@twinaphex: What you describe is neither reentrant nor non-blocking, unless one frame of emulation can be requested in the middle of running another, e.g. in two separate threads (reentrancy), or one frame of emulation can be requested and it immediately returns (i.e. doesn't block) before it's actually done (non-blocking).

Please create separate issues as appropriate to describe in detail the main differences between libretro mupen64plus and this repository and what could be done to minimise the differences, and another one for the PIC patches, for wider discussion.

This ticket has been created to discuss about if this system need to be put in the core and how it would be efficiently integrated.

Please continue this discussion here. :)

Nebuleon commented 9 years ago

Okay, so the main issue @twinaphex is saying, is that once the frontend does

CoreDoCommand(M64CMD_EXECUTE, ...);

, that's it. The frontend has given control to the emulator, and will not get it back until the ROM is closed. The issue is more about control transfer granularity than reentrancy.

Support in this repository for frontends that request one frame at a time would require:

  1. A new command for CoreDoCommand, M64CMD_EXECUTE_FRAME.
  2. Modifications to the use of the stop variable: the VI interrupt would need to set stop if the M64CMD_EXECUTE_FRAME command was used, in a different way than the use of stop for critical failures in opcodes (NI, RESERVED, MTC0, etc.) so that it can be undone before the next command. For example, stop |= STOP_END_OF_FRAME; versus stop |= STOP_CRITICAL_FAILURE;.
  3. Given that the emulation function can be repeated once it returns, any R4300 emulator that does anything before its execution would need its initialisation and its execution to be split into 2 functions. That's for things like init_blocks();, new_dynarec_init(); and <some variable> = 0xA4000040; to execute the N64 boot ROM code, which really only needs to be done once.
  4. Given that the emulation function can be repeated once it returns, any R4300 emulator that does anything after its execution would need its execution and its finalisation to be split into 2 functions. That's for things like free_blocks(); and new_dynarec_cleanup();.

3. and 4. don't cause any problems as they are entirely contained within the Core. 1. would need consideration in the API and appropriate versioning and documentation, so that frontends can continue to use M64CMD_EXECUTE. 2. could require adjustments to the asynchronous exit of the Core, for example upon Ctrl+C in mupen64plus-ui-console, and changing that use of the stop variable. It would also require changes to the New Dynarec [edit: and Hacktarux JIT]'s return from VI interrupts on all architectures to make it check for stop and return to the caller.

Would that be sufficient?

[P.S. the function is main_run, not make_run.]

wareya commented 9 years ago

that's it. The frontend has given control to the emulator, and will not get it back until the ROM is closed. The issue is more about control transfer granularity than reentrancy.

Indeed. In panui I have to run two threads just to get things rolling, and then I have no per-frame accuracy. I would love a non-blocking main execute, but that's beside this point.

The biggest problem I can see with some kind of "per frame execution from the frontend" is how the framelimiter is going to work. How does the frontend know that the core isn't still running a frame? How much work do we want the frontend to have to do with regards to waiting for the next frame when it wants this kind of control? Does it make more sense to implement the level of control a frontend would use frame-level execution control (i.e. realtime rewind) from inside the core itself? Do I even understand the problems at hand enough to talk about them?

Nebuleon commented 9 years ago

It's actually pretty much exactly on point though :)

Also, currently frameskip and synchronisation are the roles of the graphics and audio plugins, respectively. Perhaps that'd need changed, too.

wareya commented 9 years ago

I think letting the graphics and audio plugins do frameskip and sync is a good idea, but you should be able to override it with frontend-specific functions. Similar to how the transitionary audio system works.

inactive123 commented 9 years ago

1) A new command for CoreDoCommand, M64CMD_EXECUTE_FRAME.

This is something I did not do. I'll admit that the way I went about it was a bit of a dirty hack as I didn't have much reason to care about the internal core API, but I understand that for upstream we'd want to do it properly so that it fits with the rest of the emulator API, and of course I'd be happy to switch to the upstream friendly solution once done.

2) Modifications to the use of the stop variable: the VI interrupt would need to set stop if the M64CMD_EXECUTE_FRAME command was used, in a different way than the use of stop for critical failures in opcodes (NI, RESERVED, MTC0, etc.) so that it can be undone before the next command. For example, stop |= STOP_END_OF_FRAME; versus stop |= STOP_CRITICAL_FAILURE;.

You might want to check the libretro fork - similar work was done about a year ago or so. I don't pretend that any of it is perfect or necessarily done in the right way, you be the judge of that.

3) Given that the emulation function can be repeated once it returns, any R4300 emulator that does anything before its execution would need its initialisation and its execution to be split into 2 functions. That's for things like init_blocks();, new_dynarec_init(); and = 0xA4000040; to execute the N64 boot ROM code, which really only needs to be done once.

Some of this work has already been done in the libretro fork, so you might want to check it out for inspiration. Any suggestions on how to improve it or problems you spot with it are welcome.

4) Given that the emulation function can be repeated once it returns, any R4300 emulator that does anything after its execution would need its execution and its finalisation to be split into 2 functions. That's for things like free_blocks(); and new_dynarec_cleanup();.

Same here.

Also, I'd mention that the savestate code will need similar work - it will also need splitting up into several functions. Some of this work has also been done in the libretro fork. Note however that I don't do file I/O of any kind in the libretro version of mupen64plus-core, so all of the file I/O in the savestate code has been taken out. Just to let you know in advance in case your'e wondering why the code diverges from upstream there.

inactive123 commented 9 years ago

An addendum if you do intend to check out the libretro fork for inspiration: the define 'SINGLE_THREAD' has to be defined at compile time. There might still be edge cases with it.

Normally, this define is left undefined for regular builds, since there are still some issues with it. The splitting up of functions like 'main_run' has been done regardless of whether SINGLE_THREAD is defined or not.

'SINGLE_THREAD' tries to make it possible to run Mupen64plus on the same main thread that libretro is running at, effectively negating the need for a second thread to run Mupen64 on. Note however that 'SINGLE_THREAD' currently does not work with 'new_dynarec' - so a solution that makes it possible for the 'old dynarec' and 'new dynarec' to both work would have to be explored for upstream.

loganmc10 commented 8 years ago

It would be great if this could be looked at, this introduces some big portability problems. Some sort of M64CMD_EXECUTE_FRAME function would make it a lot easier to port this to other platforms (like RetroArch)