Astrabit-ST / ModShot-Core

A fork of mkxp, forked for OneShot, forked for OneShot mods, (not to be confused with the ModShot server)
https://nowaffles.com
GNU General Public License v2.0
18 stars 9 forks source link

Improve thread safety #62

Open Speak2Erase opened 2 years ago

Speak2Erase commented 2 years ago

Right now, mkxp will segfault at any point if you try to call Graphics.update or the like from another thread. This is quite irritating if you want to unhook game logic from the framerate. From what I can tell, it looks like this is in part due to any thread aside from the main thread lacking the OpenGL context.

I'm not sure just yet what the solution to this would look like, or if its even possible. More likely than not, someone with more experience (@GamingLiamStudios or @thehatkid?) may be better suited to look at this problem.

servantoftestator commented 2 years ago

Would disabling error handling in opengl make any difference https://stackoverflow.com/questions/56824376/can-i-disable-automatic-error-handling-in-opengl ? The simplistic solution would be disabling error handling in opengl and hoping the undefined behaivor doesn't have the context exit.

Otherwise you'd have to decouple the sdl video functions from the input functions in eventthread.h and make a second struct in eventthread.h for seperation followed by calling it appropriately throughout the codebase. Graphics::update and the likes manipulate RGSSThreadData which is defined at src/threads/headers/eventthread.h . RGSSThreadData takes input, is declaring the window functions, and using sdl to tie the thread to the framelimit. List of places RGSSThreadData is called..... https://pastebin.pl/view/5fe87dbd

Speak2Erase commented 2 years ago

If you call print in ruby somewhere down the line it ends up calling Graphics.update on a thread that doesn't hold the OpenGL context, leading to a segfault

zimberzimber commented 2 years ago

Wouldn't hijacking Graphics.update to check if it's on the main thread before continuing solve this?

servantoftestator commented 2 years ago

in src/graphics/source/graphics.cpp One of the functions of the class Graphics, is Graphics::Graphics(RGSSThreadData *data) which calls the private struct GraphicsPrivate . Even if you checked if you are in RGSSThreadData you'd have to exit RGSSThreadData and re-enter it to cleanly make that transition. Which means forfieting input, framerate, and more importantly the window that is tied up in RGSSThreadData while you exit/re-enter. I'd imagine the usecase for calling the main thread, RGSSThreadData, from ruby would still desire user input with a window, just without the framerate tied to it.

So you'd have to decoupled the portions of RGSSThreadData that you do not desire in that context in eventthread.h and not store it on line 673 where it brings up opengl context in src/graphics/source/graphics.cpp. Otherwise you'd have to get the OpenGL context to save to a buffer temporarily and then re-call it on seperately from what RGSSThreadData does using sdl. Problem is that the functions RGSSThreadData as a struct and graphics.cpp uses are in a private struct for the opengl context bringup so you'd have to seperate them out anyways at line 673 in graphics.cpp.

Speak2Erase commented 2 years ago

Wouldn't hijacking Graphics.update to check if it's on the main thread before continuing solve this?

No, because it calls the C++ side equivalent. At some point down the line print calls this function which calls shState->graphics().repaintWait(), which is effectively a gimped Graphics.update

servantoftestator commented 2 years ago

Yea at https://github.com/Astrabit-ST/ModShot-Core/blob/718e6d5a3e908ab0a595a826cff9348f5f80a7bd/src/graphics/source/graphics.cpp#L1274 Its retuning an undefined parameter since exitCond is already met when the graphics context is not available. So when shState->graphics().repaintWait(msgBoxDone); is called it repaints a undefined value since the graphics haven't been brought up for this usecase.

servantoftestator commented 2 years ago

In EventThread::showMessageBox you might be able to get away with a if; then statement for shState->graphics().repaintWait(msgBoxDone); . Not sure what you would if for though.... You could also make Graphics::repaintWait Return something instead of a undefined value as it currently stands. See the bottom example https://www.geeksforgeeks.org/return-statement-in-c-cpp-with-examples/

The real solution here is rewriting in rust instead of dealing with memory management like this ;p .

Speak2Erase commented 2 years ago

There is a rewrite in rust ongoing, but it is very slow unless we get more people contributing.

servantoftestator commented 2 years ago

repaintWait only has two users and then its defines in graphics.h/cpp binding-mri/binding-mri.cpp: shState->graphics().repaintWait(shState->rtData().rqResetFinish, src/thread/source/eventthread.cpp: shState->graphics().repaintWait(msgBoxDone); Looks like your ruby print call is routed through binding-mri.cpp specifically called at static void processReset(). You also could put a if; then statement in binding-mri.cpp to only call repaintWait if something....

Speak2Erase commented 2 years ago

This really isn't addressing the main problem, putting an if is more of a bandaid than anything

Speak2Erase commented 2 years ago

I do appreciate the effort though

GamingLiamStudios commented 2 years ago

The real solution here is rewriting in rust instead of dealing with memory management like this ;p .

but then you have to deal with the syntax of Rust

GamingLiamStudios commented 2 years ago

Right now, mkxp will segfault at any point if you try to call Graphics.update or the like from another thread. This is quite irritating if you want to unhook game logic from the framerate. From what I can tell, it looks like this is in part due to any thread aside from the main thread lacking the OpenGL context.

OpenGL isn't very good at running in multiple threads, so you've either gotta keep ALL graphical calls in the main thread or rewrite in something that is a bit more multithreading friendly, like Vulkan(but that comes with its own set of challenges).

I think the best solution rn is to make a very basic event system for the Graphical context. You'd have a thread-safe queue, which the main graphical thread would be constantly waiting for information from, and the other threads would add a graphical event it wants, like moving a sprite or showing some text.

Would be a pain in the ass to create, but once you've got the ruby boilerplate shit in, it should be mostly seamless.

GamingLiamStudios commented 2 years ago

You could also make the Graphics.update only run on the main thread, the other calls sending a hint to the main thread to run it. Not sure how much OpenGL will like that tho.

servantoftestator commented 2 years ago

You could also make the Graphics.update only run on the main thread, the other calls sending a hint to the main thread to run it. Not sure how much OpenGL will like that tho.

If you implemented it like that you would have to make all the calls into RGSSThreadData into thread safe futex's or windows equivielent to play nicely with opengl. I know that in x86/64 assembly for linux that spinlock implementations are much slower then plain register transfer logic. As futex/mutex uses a clear which is slow in x86 due to the nature of OOE processor memory models. Also see https://www.khronos.org/opengl/wiki/OpenGL_and_multithreading

Speak2Erase commented 2 years ago

I believe mkxp-z does that. Will look into.