Ancurio / mkxp

Free Software implementation of the Ruby Game Scripting System (RGSS)
GNU General Public License v2.0
532 stars 140 forks source link

Compiling with Emscripten #195

Closed pulsejet closed 6 years ago

pulsejet commented 6 years ago

Haven't fully looked into this yet, but it might be possible to compile mkxp with Emscripten to run in modern browsers. So far, I can find that SDL might work, mapping OpenGLES calls to WebGL, and someone has successfully compiled MRI 1.8 here. That mostly leaves looking into non-existent shared state support for JS threading and platform specific code. Any ideas on this?

Ghabry commented 6 years ago

I tried around one year ago to port mkxp to emscripten but I was not really successful. In the end I was able to see the title screen of Desert Nightmare (game I used for testing) but then the browser froze because it ran too slow. Therefore OpenGLES -> WebGL mapping just works.

tl;dr: If you want to work on it I can share my current progress with you.

Ruby 1.8 can be simply crosscompiled via emscripten. The main problem is that Ruby 1.8 has lots of undefined behaviour, it calls function pointers with the wrong amount of arguments. This works in the x86 calling convention but emscripten asserts. I manually patched all the incorrect function calls (that took a while) until mkxp finally started in the browser without asserts. Though still half of the time the ruby library miscompiled and failed with strange error messages, probably caused by more undefined behaviour. I plan to redo the ruby port with help of UBSan (undefined behaviour sanitizer).

Another problem is that the script must yield (return to the browser), but the sleep in mkxp is done in the Graphics function that is deep in the Ruby callstack. I created a list of functions that are on the stack when this happens and added them to the EMTERPRETER list, unfortunately this way basicly half of the ruby code runs through the slow emterpreter :/.

To get rid of the browser hangs you could use Webworkers but in all my previous SDL tests I was never able to get the input working and webworkers have a limited API, OpenGLES2 isn't supported by the emscripten-worker-proxy, therefore mkxp probably won't work in them at all :/.

In Mkxp itself I patched nothing, except for the boost dependency because boost is bloat and is only used for config-file parsing which is not too useful in the web.

pulsejet commented 6 years ago

An idea I had in mind was to use mruby instead, which might have better results. Also, did you discover why the game was freezing? I remember reading somewhere that MRI compiled with emscripten has some severe memory leaks.

Ghabry commented 6 years ago

Unfortunately mruby does not implement the whole standard library and is not compatible with Ruby 1.8 this means most games won't run because of syntax errors or missing modules.

I guess the reason for the freeze was just that the program ran too slow and didn't reach "emscripten_sleep" often enough per frame. Also I only got the debug build to work, the release build always crashed.

pulsejet commented 6 years ago

Other than the missing functionality like Marshal (which can be added with mrbgems), I don't see anything specific listed in mruby limitations. Is this document incomplete or am I missing something here?

Ghabry commented 6 years ago

Sorry, I can't really answer that question because the last time I used mruby was years ago. :/

Ancurio commented 6 years ago

Other than the missing functionality like Marshal (which can be added with mrbgems), I don't see anything specific listed in mruby limitations. Is this document incomplete or am I missing something here?

Marshal for mruby is already implemented in mkxp. Overall the mruby-backend is in a state where you can run a bare-bones RPG Maker XP "New Project" game, but last time I checked battles didn't work due to some callback code not working correctly under mruby.

There's a ton missing in mruby, and some types of syntax is implemented differently (the Ruby Specification explicitly says "behavior implementation defined").

Technically mruby offers you everything you need to write RPG Maker Games, but the reality is that 99% of games depend on such a mountain of 1.8 specific behavior that it's not feasible to run them via mruby without a rewrite of the scripts.

Mind you, last time I tried mruby with real games was 3 years or so ago.

pulsejet commented 6 years ago

Interesting ... mruby's readme does explicitly state that the syntax is compatible with MRI 1.9 though. I'll try it out (before the month ends, hopefully :P) and report back if the are any new findings then. Another reason I wanted to use mruby was that it was created for something like this in the first place i.e. this is the perfect use case scenario ...

Ancurio commented 6 years ago

https://www.ipa.go.jp/files/000011432.pdf (I think that's the ISO one?), 12.5 (while statement), a):

If S is a begin-expression, the behavior is implementation-defined.

And one of the default scripts used a while / begin combo somewhere that behaved differently under MRI 1.8 and mruby.

(Same with until modifier)

pulsejet commented 6 years ago

Either way, if it works with a fairly limited set of changes, I'm still okay with it since full compatibility would be impossible to achieve anyway

pulsejet commented 6 years ago

Got mkxp to build with emscripten with everything linked statically, but since mkxp uses SDL_WaitEvent to wait for events which is incompatible with emscripten, some logic rewrite would be necessary ...

Ancurio commented 6 years ago

What is the chosen path for rewriting a sleeping input loop into one with PollEvent? Do you just spin endlessly, or is there some way to give control back to the browser? I have 0 experience with this.

Ghabry commented 6 years ago

@pulsejet Yepp compiling is easy, the difficult part is getting it to run. :D

In my emscripten port attempts I got rid of all threads in mkxp (threading not supported) and replaced it with function calls to the thread functions in the mainloop and some other ugly workarounds :). I can upload the code when I'm at my dev PC later. Though I'm not sure if I took care of SDL_WaitEvent, good find!

@Ancurio The emscripten application must always returns back to the browser. The recommended way to do it is to use emscripten_set_main_function which is called 60 times per second (and must return). Though when the main event loop is deep inside the callstack or, worse, can be called recursive, this is very difficult to do. (like mkxp in that case)

Another way to yield back to the browser is emscripten_sleep but for this to work the callstack must not contain any native function that is not Emterpreted. The emterpreter is some byte code interpreter. You basicly pass a name of all functions that shall be emterpreted (kinda slow) and when all functions up to the Graphics handling code (which is called by some ruby code) are empterpreted you can call emscripten_sleep instead of SDL_sleep to return to the browser event loop.

pulsejet commented 6 years ago

@Ghabry would love to have a look at any changes you had done :+1: A question, how did you build and link the libraries in? The way I'm doing it right now is to compile everything to bytecode and then manually link everything in. I'm using most of the ports provided here, but unfortunately there is no jpeg, so I still need to do SDL_image manually.

(btw, any of you guys seen tapir yet? It's a really recent parallel to mkxp, but MIT-Apache, allowing it to go iOS. Some pretty bad 2-space indented code in there, but it is written completely in C, works surprisingly well and uses a really small set of libraries)

Ghabry commented 6 years ago

okay I will try to find my code, havn't looked at it for a year.

Correct you crosscompile all libraries to bitcode by using emconfigure/emcmake and then you link them in.

I heard about tapir before but havn't taken a look at it yet, maybe is easier to port? No idea.

Ghabry commented 6 years ago

Here is my WIP code (based on my MRI 1.8 branch because I used ruby 1.8). Without support, is ugly :)

https://github.com/Ghabry/mkxp/tree/emscripten-mri-1.8

Removed all threading and hardcodes all emscripten stuff in CMakeLists.txt. Additionally removes boost because I didn't want to crosscompile this bloat :)

When you take all the source (w/o CMakeLists.txt) it will also build for Linux and just work. This way you can verify that removing the threads worked.

pulsejet commented 6 years ago

@Ghabry had to patch up a couple of files to work around some PHYSFS errors, but this compiles quite smoothly otherwise. Now when I run it, I get something like

Invalid function pointer '0' called with signature 'ii'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)

on the first rb_define_class call. I did try compiling with ASSERTIONS=2 as suggested somewhere, but it gives the same result. Could it be that my mri is badly compiled? I'm guessing this is an equivalent of a segmentation fault?

pulsejet commented 6 years ago

Wait I'm a dummy. That segfault is now happening even in my Linux build

Ghabry commented 6 years ago

Oh guess my branch was not rebased for latest physfs changes. These are problems in ruby 1.8 caused by undefined behaviour in this case calling a function pointer with the wrong amount of arguments. You must fix all incorrect function calls in ruby 1.8 (a lot). The stacktrace tells you where it failed. When you compile the x86 ruby 1.8 with ubsan it should tell you all bad function pointers and other issues while running mkxp. But havnt tested this yet.

pulsejet commented 6 years ago

Gave up on MRI, tried fixing some mruby instead :smile: . Nothing works yet (of course), but its a start :stuck_out_tongue: Screenshot_from_2018-04-30_21-39-44.png

Ghabry commented 6 years ago

Great, Looks like my old MRI 1.8 progress, title Screen visible :). Now you only need to add all functions that are on the stack before emscriten_sleep is called to the EMTERPRETER_WHITELIST and it should kinda work

pulsejet commented 6 years ago

@Ghabry #ifdef is evil. I believe your code died here https://github.com/Ghabry/mkxp/blob/5b3c1d2b13764abedb4b903ef55cdbc16230826e/src/graphics.cpp#L449 Emscripten defines __EMSCRIPTEN__ :upside_down_face: EDIT: In other news, input works now

pulsejet commented 6 years ago

Now I need to figure out why the game won't start with mruby on x86 on this branch

Ghabry commented 6 years ago

Yes it takes a different codepath because you can't use SDL_Delay :).

Please read the wiki page about the Emterpreter https://github.com/kripken/emscripten/wiki/Emterpreter

pulsejet commented 6 years ago

I'm not sure you caught on what I was saying. The emscripten_sleep on line 450 was never being called because __EMSCRIPTEN is not defined (notice the missing trailing underscores).

Ghabry commented 6 years ago

wups, you are right, my bad 👎. Now I have to retest with MRI 1.8 :D

pulsejet commented 6 years ago

Is there any way I can test changing assets without linking everything again? Currently, it takes ~3min for every change I make for the linking. Btw, I'm preloading assets instead of embedding. Also, mruby compiled for x86 and emscripten seem to have some differing behavior. Is this expected?

pulsejet commented 6 years ago

Okay this is really weird now ... the game crashes with stock scripts but if I add a bunch of print messages, it suddenly starts working ... maybe more sleeps might help? Also for some reason, movement doesn't work on map EDIT: menu is also frozen Screenshot_from_2018-04-30_23-38-52.png

Ghabry commented 6 years ago

Puh no idea, maybe hangs in another endless loop due to how the logic of Scene_Menu is?

Because you got it kinda working I'm also interested in taking a look again :D Is it enough to use your PR https://github.com/Ancurio/mkxp/pull/197 and link against mruby?

pulsejet commented 6 years ago

Yeah, #197 is mostly it (don't think there was any other change, but my local tree is too dirty to be sure). Actually, the player is moving on the map when I press keys, but the screen is not updated. This way, when I open the menu and go back, the player is at the new position. What different happens during the transition?

pulsejet commented 6 years ago

Menu is working now. The culprit was the biggest breaking change in mruby, the fact that 1/2 is 0.5 and not 0. The update method for Window_PlayTime looks liks

def update
    super
    if Graphics.frame_count / Graphics.frame_rate != @total_sec
      refresh
    end
  end

and so it triggers a refresh continuously on mruby as against MRI. @Ancurio would this be worth adding to the README, since this affects native builds as well?

Ghabry commented 6 years ago

There is a patch for configuring integer divison behaviour of mruby here: https://patch-diff.githubusercontent.com/raw/WaveformDelta/mruby/pull/2.patch

When you apply it and add conf.cc.defines = %w(MRB_INTEGER_DIVISION) to MRuby::Build.new do |conf| in "build_config.rb" you get:

mirb - Embeddable Interactive Ruby Shell

> 1/2
 => 0
Ancurio commented 6 years ago

@pulsejet I believe I covered things like this already :) From the readme:

Due to heavy differences between mruby and MRI as well as lacking modules, running RPG Maker games with this binding will most likely not work correctly.

pulsejet commented 6 years ago

Woot! Woot! https://www.youtube.com/watch?v=ScGNIOC08d0 Turns out the fix was to disable frameSkip. I'm guessing the way it is implemented, it is causing all frames to be dropped on the map.

pulsejet commented 6 years ago

~Btw @Ghabry you will need to build boost for the mruby bindings, since the marshal class needs it~

Ancurio commented 6 years ago

Btw @Ghabry you will need to build boost for the mruby bindings, since the marshal class needs it

The only dependency I see is boost-hash, which is a header-only library, no compilation involved (and that class could soon be transitioned to C++11 anyway).

pulsejet commented 6 years ago

Oh right :man_facepalming: You just saved me a lot of trouble :smile: @Ghabry could you get SDL_sound to compile with MP3 support? make fails with a huge bunch of errors if I try

pulsejet commented 6 years ago

https://pulsejet.github.io/mkxp-mruby-emscripten-demo/ Got the minimal project running with minimal changes (like I said in an earlier comment, some prints are necessary for no apparent reason).

~For some reason, GitHub Pages doesn't compress WASM, so download size is 8mb including the assets, but if properly gzipped, it should go to 4mb.~ Performance is near-native, but I do suspect there is a memleak lurking somewhere, though I'm not really sure.

This uses every possible optimization except closure, just cause I'm too lazy to install java on my system

EDIT: GitHub Pages does compress WASM (significantly at that). Something was wrong with my browser I guess. Total download size for the minimal project is now 3.4mb. Out of this, 1.5mb is the assets, so mkxp is less than 2mb :smiley:

pulsejet commented 6 years ago

Here's my (probably incomplete) list of emterpreted functions. Making this is really painful ...

["_main", "__Z13rgssThreadFunPv", "__ZL17mrbBindingExecutev", "_mrb_load_nstring_cxt", "_mrb_load_exec", "_mrb_top_run", "_mrb_vm_run", "_mrb_vm_exec", "__ZL18graphicsTransitionP9mrb_state9mrb_value", "__ZN8Graphics10transitionEiPKci", "__ZN15GraphicsPrivate12swapGLBufferEv", "__ZL14graphicsUpdateP9mrb_state9mrb_value", "__ZN8Graphics6updateEv", "__ZL11inputUpdateP9mrb_state9mrb_value", "__ZN11EventThread7processER14RGSSThreadData", "_emscripten_sleep__wrapper", "_Emscripten_HandleMouseButton", "_Emscripten_HandleMouseMove", "_Emscripten_HandleKey", "_Emscripten_HandleFocus", "_SDL_SendMouseButton", "_SDL_PrivateSendMouseButton", "_Emscripten_HandleKeyPress", "_SDL_SendKeyboardKey", "_SDL_SendWindowEvent", "_SDL_EventState", "_SDL_ResetKeyboard", "_Emscripten_HandleMouseFocus", "_SDL_SendMouseMotion", "_SDL_UpdateMouseFocus", "_Emscripten_HandleVisibilityChange", "_SDL_PushEvent", "_SDL_GetTicks", "_SDL_SendKeyboardText", "_SDL_GetWindowSize", "_SDL_OnWindowFocusLost", "_SDL_PeepEvents", "_SDL_GetMouse", "_SDL_AtomicGet", "_SDL_AtomicAdd", "_SDL_PrivateSendMouseMotion", "_SDL_OnWindowHidden", "_SDL_OnWindowShown", "_SDL_OnWindowFocusGained", "_SDL_GestureProcessEvent", "_SDL_SetMouseFocus", "_SDL_malloc", "_malloc", "_Emscripten_HandleResize", "_SDL_GetMouseFocus", "_Emscripten_ShowCursor", "_SDL_utf8strlcpy", "_SDL_realloc", "_realloc", "_SDL_OnWindowEnter", "_SDL_UpdateFullscreenMode", "_SDL_memset", "_try_realloc_chunk", "_SDL_GetWindowDisplayIndex", "_Emscripten_HandleWheel", "_SDL_GetDisplayBounds", "_SDL_SendMouseWheel", "_free", "_SDL_EnclosePoints", "_SDL_abs"]

Performance does dip down quite a bit to a point where it becomes unplayable when the map becomes bigger with more events, though there are a few things that might be worth looking into, including disabling C++ exceptions (which works with the minimal project) and I don't really remember much of them, but any changes from my fork for mobile devices. Another thing might be any other differences between mruby and mri. Any other suggestions?

pulsejet commented 6 years ago

@Ghabry do you by any chance have your fixed MRI 1.8 code lying around? I really am no good at this sort of thing.

Ghabry commented 6 years ago

@pulsejet

could you get SDL_sound to compile with MP3 support? make fails with a huge bunch of errors if I try

Havn't tried it yet.

About your Emterpreter list: When you compile with -s ASSERTIONS=0 you can remove all the SDL-functions that call into the WASM code through callbacks because the callback functions don't call emscripten_sleep. It will still work (though without Assertions which is not always ideal) and probably perform better :). But thanks for the list, I was too lazy to create a complete list of all SDL functions :D.

You are lucky because I just prepared a repository and wasted my whole last evening & night to redo the MRI 1.8 patching from the beginning :D.

https://github.com/Ghabry/ruby-1.8-emscripten

Emterpreter function list: https://github.com/Ghabry/mkxp/blob/emscripten-mri-1.8/CMakeLists.txt#L58

Some games will need -s DISABLE_EXCEPTION_CATCHING=2 otherwise it will crash before the title screen. Maybe you can find a fix for this.

It basicly works I executed a RPG Maker XP Project and "Desert Nightmare R". They both ran really slow but I wasn't able to compile with anything better than "-Os" because I don't have enough RAM for the optimizer :). (my resulting .js file, didn't use WASM, is 26 MB and 3,3 MB gzipped...).

Here the two games for testing: https://cloud.mastergk.de/web/mkxp/Project1/ https://cloud.mastergk.de/web/mkxp/Desert/

Marshall/Save doesn't work yet, more bad function pointers...

EDIT: Got a -O2 build but the code miscompiles and fails with "[BUG] terminated node" :(

And the asm.js has a validation error which means it doesn't even run through asm.js but through the normal JS-JIT. Probably WASM will be faster :)

pulsejet commented 6 years ago

Awesome! I tried compiling with O3 first, but that doesn't seem working for me either and no error is shown. Gonna try with other levels now, though I can't imagine why this could be happening. The performance is similar to what I have with mruby for low optimization, so yeah, it should be much faster with WASM :D

EDIT: I'm getting (eval):1475: [BUG] terminated node (0xe43900) with O0 as well ...

EDIT 2: Okay if I compile mkxp without optimization then it works

The line in binding-mri.cpp that loads the scripts throws an error at higher optimizations (which is not caught?)

Ghabry commented 6 years ago

Any idea how to debug this to find the problem? Not reproducible while running normally at my PC, not even with icall sanitizer enabled. Before I already wasted hours on "FileInt can't converted to Integer" which was in the end one incorrect function pointer in marshall.c. :/

pulsejet commented 6 years ago

There is an ugly workaround to this. Change the line where the bug is thrown to a printf (in eval.c, if I remember right) and add __attribute__ ((optnone)) to runRMXPScripts in binding-mri.cpp. This allowed me to compile with O3 for everything and wasm (even asm.js validation passes). Interestingly, there isn't much difference in runtime performance (it is still really slow), though startup time is much better. Sizes before gzip are 4.4mb (wasm) + 400kb (js) + 400kb (emterpreted binary)

EDIT: CPU is bottlenecking (maxes out for me); maybe something here might be useful

EDIT2: Nope, it seems to be rb_eval that is slow - probably emterpreter :( (says Gecko profiler)

pulsejet commented 6 years ago

@Ghabry is the sleep in eventthread necessary? Everything seems working without it as well, though I'm not sure how.

Ghabry commented 6 years ago

Cool, thanks for finding a workaround. I tried to find the issue via "-s SAFE_HEAP" but this results in so many crashes, would take hours to reach the eval function :/.

Due to the RPG Maker script design where even the main loop is under ruby control I don't think it will be possible to not emterpret this gigantic eval function :(. Though I wonder why mruby is much faster, less gigantic functions?

One single emscripten_sleep in the Graphics-Update code should be enough.

pulsejet commented 6 years ago

So I decided to really take it a step further and take the main loop out of ruby. The way I'm going about this is to have a callback function in the RGSS scripts which will be called by mkxp for every frame. This allows having a separate main loop, and thus emterpreter just goes out of the equation, so literally everything is running in asm.js (I'm unable to compile to wasm due to lack of memory, I guess) . For my working tree, I have much better performance (it is really playable).

As far as changes in the scripts are concerned, this loop is executed by mkxp

$prev_scene = nil

def main_update_loop
  if $scene != nil
    if $scene != $prev_scene
      if $prev_scene != nil
    $prev_scene.dispose
      end
      $scene.main
      $prev_scene = $scene
    end
    # Update game screen
    Graphics.update
    # Update input information
    Input.update
    # Frame update
    $scene.update
  else
    raise "END"
  end
end

So for every scene, you need a dispose method which is the code that is usually below the scene's main loop and just remove the main loop from all scenes. With the new project, it takes less than 2 min to make these changes.

EDIT: There still are a lot of stability issues in the sense that the game crashes randomly (sometimes because of a uncaught longjmp call etc.). Could these be bad function calls as well, or something to do with the workaround?

Ghabry commented 6 years ago

Wow, this is some serious work around :D. When the logic is always "move stuff after update loop to dispose" this could be even monkey-patched automatically... hacky.

When it is a bad function pointer you should be able to catch it via -s ASSERTIONS=2 -s ALIASING_FUNCTION_POINTERS=0. setjmp itself appears to be implemented in emscripten 👍

@Ancurio I'm currently not at my dev PC but I just found a bug in runRMXPScripts. Maybe it fixes the random crashes when the optimizer is on? That line casts away the const modifier from c_str(): https://github.com/pulsejet/mkxp/blob/01cf9243cf870abf619c536fddc6c4da185506b5/binding-mri/binding-mri.cpp#L447 Correct would be to make decodeBuffer a std::vector and change c_str() to data() (and remove the const_cast obviously) The required output buffer size can be precalculated via the zlib function compressBound(RSTRING_LEN(scriptString)) btw.

pulsejet commented 6 years ago

Funnily enough, I was just running out of memory :P. Setting it to 256MB fixes all (as far as I have tested) crashes. WASM might be able to help here, since its performance is unaffected when memory growth is allowed. I'm guessing the longjmp error was occuring because on running out of memory, ruby tried to go to the stack of some call before the main loop, which has already unwound due to the async nature of the calls.

EDIT: There's probably a memory leak somewhere, since it still crashes after some time. Any ideas where that might be/how to find it?

EDIT 2: Something is really broken in wasm. Travis failed to compile it with 8G RAM in an hour

EDIT 3: Garbage collection is broken. There is some incorrect call on GC.enable and GC.start that kills it I think

Confirmed the memory leak (just link with --memoryprofiler).

pulsejet commented 6 years ago

Actually the garbage collector is working, but only when linking with O0 (I think it's broken with asm.js). It's also terribly slow when interpreted, but does work.

EDIT: With simulate_infinite_loop, the garbage collector no longer freezes with O3, but it still isn't working. On the positive side, even puts statement was causing a memory leak earlier, now it isn't.

pulsejet commented 6 years ago

Ported a real game to the web :D https://pulsejet.github.io/knight-blade-web/ This uses mruby. As for the changes I had to make to the scripts, one was the loop change in battle similar to this, another was to remove all monkey patching, since behavior of calling super in monkey patched methods differs from MRI. Takes some time to load since it is around 30mb, and saving doesn't work (need work on the marshal class), but I don't believe there is any memleak and frame rate seems acceptable.

EDIT: Got saving working, shifting to take-cheeze's marshalling mrbgem. Gonna try to maintain a list of changes needed at https://gist.github.com/pulsejet/bbaf3f043ffee1146174159cae042f74

Either way, I don't see anything useful that could be changed upstream to help in this, so I'll close this. The next things I'm gonna look into are lowering CPU usage, lazy loading (this one is especially important) and trying to fix the MRI 1.8 memleak. Thanks @Ghabry @Ancurio for your help!