-
Notifications
You must be signed in to change notification settings - Fork 140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compiling with Emscripten #195
Comments
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. |
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. |
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. |
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? |
Sorry, I can't really answer that question because the last time I used mruby was years ago. :/ |
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. |
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 ... |
https://www.ipa.go.jp/files/000011432.pdf (I think that's the ISO one?),
And one of the default scripts used a (Same with |
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 |
Got mkxp to build with emscripten with everything linked statically, but since mkxp uses |
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. |
@pulsejet 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. @Ancurio 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. |
@Ghabry would love to have a look at any changes you had done 👍 (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) |
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. |
Here is my WIP code (based on my MRI 1.8 branch because I used ruby 1.8). https://github.com/Ghabry/mkxp/tree/emscripten-mri-1.8 Removed all threading and hardcodes all emscripten stuff in CMakeLists.txt. 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. |
@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
on the first |
Wait I'm a dummy. That segfault is now happening even in my Linux build |
Oh guess my branch was not rebased for latest physfs changes. |
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 |
@Ghabry |
Now I need to figure out why the game won't start with mruby on x86 on this branch |
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 |
I'm not sure you caught on what I was saying. The |
wups, you are right, my bad 👎. Now I have to retest with MRI 1.8 :D |
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. |
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 |
The only dependency I see is |
Oh right 🤦♂️ You just saved me a lot of trouble 😄 |
https://pulsejet.github.io/mkxp-mruby-emscripten-demo/
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 😃 |
Here's my (probably incomplete) list of emterpreted functions. Making this is really painful ...
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? |
@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. |
Havn't tried it yet. About your Emterpreter list: 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 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: Marshall/Save doesn't work yet, more bad function pointers... EDIT: 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 :) |
Awesome! I tried compiling with EDIT: I'm getting EDIT 2: Okay if I compile mkxp without optimization then it works The line in |
Any idea how to debug this to find the problem? Not reproducible while running normally at my PC, not even with icall sanitizer enabled. |
There is an ugly workaround to this. Change the line where the bug is thrown to a EDIT: CPU is bottlenecking (maxes out for me); maybe something here might be useful EDIT2: Nope, it seems to be |
@Ghabry is the sleep in eventthread necessary? Everything seems working without it as well, though I'm not sure how. |
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. |
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 EDIT: There still are a lot of stability issues in the sense that the game crashes randomly (sometimes because of a uncaught |
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 @Ancurio |
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 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 |
Actually the garbage collector is working, but only when linking with EDIT: With |
Ported a real game to the web :D https://pulsejet.github.io/knight-blade-web/ 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! |
@pulsejet That's really cool! :D |
Fluidsynth can't be really used because the soundfonts must be downloaded. Are there any small soundfonts that don't sound bad? |
I might even have been okay with downloading the soundfont, but I'm more worried about the overhead, since CPU usage is already very high. For some reason, BGM (and BGS) doesn't work at all, including ogg, while ogg in SE works; still need to figure this out. |
Thought I'd mention here, BGM, BGS and ME aren't working since a separate thread is spawned for an |
Just another update, on my branch at https://github.com/pulsejet/mkxp/tree/mruby-emscripten (sorry this has become incredibly dirty), I managed to get asynchronous loading of graphics working. The way I went about this is to have placeholders for bitmaps (basically to allow the scripts to know what size the bitmap would be without waiting for it to load; I know this can be done in a better manner) and load and refresh them with a callback with emscripten's fetch API. SE audio loads in a similar manner, with no placeholder. Knight blade with async loading at https://pulsejet.github.io/knight-blade-web-async/ =D EDIT: By tricking GitHub Pages to gzip the binary filesystem (just by renaming it to index.txt from index.data), the total download size till the title screen is just 3.4MB |
Wow this is really impressive that you got this working :) emscripten async fetch + callback Is basicly how we do it in EasyRPG Player (for 2k and 2k3) but we have the advantage, that no scripting exists so we have full control about the asset loading process (and there are no script functions which can read width or height of an image). When this got more polish the ones from rmarchiv.tk will be happy because we already provide web players for all 2k, 2k3 and MV games 👍 Guess I have to take a look at MRI + Web again now :D |
Haha I'm going to use EasyRPG as reference the next time I do anything. Spent some time trying to figure out how to get sockets working for sync loading.
This is exactly what I had in mind but I'm too lazy to implement it right now. @Ancurio @Ghabry any idea how audiostream (for bgm/bgs/me) could be converted to not using a thread (make it like sound emitter)? I've zero experience with OpenAL. |
Well here for reference our async handler code: CreateRequestMapping parses the index.json, RequestFile opens a file request and Start executes it. The class remembers which files were already downloaded, so they are only downloaded once. and our index.json generator:
Unfortunately not, we use the SDL audio API, so have no idea how to implement this. |
From the top of my head: There is one threaded class, |
@Ancurio @Ghabry I have "completed" a web port of mkxp in some sense here. A fully functional port (with audio and saving in all its glory) of knight blade here. Some notes (random or otherwise):
I had to patch mruby manually to get integer division to be consistent (the patch linked above no longer works). For the record, my build config looks like this: MRuby::CrossBuild.new('x86_64-pc-linux-gnu') do |conf|
toolchain :clang
conf.gembox 'default'
conf.gem :github => 'take-cheeze/mruby-marshal'
conf.gem :github => 'monochromegane/mruby-time-strftime'
conf.gem :core => 'mruby-eval'
conf.cc.command = 'emcc'
conf.cc.flags = %W(-O3 -g0)
conf.cxx.command = 'em++'
conf.cxx.flags = %W(-O3 -g0)
conf.linker.command = 'emcc'
conf.archiver.command = 'emar'
end |
Great work, I'd like to pick this up somehow. I was considering a smallish rewrite that would help with the threading issue, but I don't have time for it right now |
Awesome! As such, in my testing, everything just works. I've a playable version of Exit Fate (which has a lot of custom scripts) at https://exitfate.radialapps.com that optionally uses Google Drive to preserve saved games (with fallback to IndexedDB) |
For anyone who might want to take this up, I've created a fork with a nice build script at https://github.com/pulsejet/mkxp-web. The sample game (Knight Blade) is continuously deployed from master to GitHub pages (here) using GitHub Actions CI The readme also has a porting section for mruby here |
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?
The text was updated successfully, but these errors were encountered: