Skip to content
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

Closed
pulsejet opened this issue Apr 21, 2018 · 63 comments
Closed

Compiling with Emscripten #195

pulsejet opened this issue Apr 21, 2018 · 63 comments

Comments

@pulsejet
Copy link

pulsejet commented Apr 21, 2018

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
Copy link

Ghabry commented Apr 22, 2018

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
Copy link
Author

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
Copy link

Ghabry commented Apr 22, 2018

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
Copy link
Author

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
Copy link

Ghabry commented Apr 22, 2018

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

@Ancurio
Copy link
Owner

Ancurio commented Apr 23, 2018

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
Copy link
Author

pulsejet commented Apr 23, 2018

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
Copy link
Owner

Ancurio commented Apr 23, 2018

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
Copy link
Author

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
Copy link
Author

pulsejet commented Apr 28, 2018

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
Copy link
Owner

Ancurio commented Apr 29, 2018

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
Copy link

Ghabry commented Apr 29, 2018

@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
Copy link
Author

pulsejet commented Apr 29, 2018

@Ghabry would love to have a look at any changes you had done 👍
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
Copy link

Ghabry commented Apr 29, 2018

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
Copy link

Ghabry commented Apr 29, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

@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
Copy link
Author

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

@Ghabry
Copy link

Ghabry commented Apr 30, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

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

@Ghabry
Copy link

Ghabry commented Apr 30, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

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

@pulsejet
Copy link
Author

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

@pulsejet pulsejet reopened this Apr 30, 2018
@Ghabry
Copy link

Ghabry commented Apr 30, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

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
Copy link

Ghabry commented Apr 30, 2018

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

@pulsejet
Copy link
Author

pulsejet commented Apr 30, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

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
Copy link

Ghabry commented Apr 30, 2018

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 #197 and link against mruby?

@Ancurio
Copy link
Owner

Ancurio commented Apr 30, 2018

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
Copy link
Author

pulsejet commented Apr 30, 2018

Oh right 🤦‍♂️ You just saved me a lot of trouble 😄
@Ghabry could you get SDL_sound to compile with MP3 support? make fails with a huge bunch of errors if I try

@pulsejet
Copy link
Author

pulsejet commented Apr 30, 2018

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 😃

@pulsejet
Copy link
Author

pulsejet commented May 1, 2018

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
Copy link
Author

pulsejet commented May 1, 2018

@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
Copy link

Ghabry commented May 1, 2018

@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
Copy link
Author

pulsejet commented May 1, 2018

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
Copy link

Ghabry commented May 1, 2018

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
Copy link
Author

pulsejet commented May 2, 2018

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
Copy link
Author

pulsejet commented May 2, 2018

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

@Ghabry
Copy link

Ghabry commented May 2, 2018

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
Copy link
Author

pulsejet commented May 3, 2018

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
Copy link

Ghabry commented May 3, 2018

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
Copy link
Author

pulsejet commented May 3, 2018

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
Copy link
Author

pulsejet commented May 4, 2018

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
Copy link
Author

pulsejet commented May 4, 2018

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!

@pulsejet pulsejet closed this as completed May 5, 2018
@Ancurio
Copy link
Owner

Ancurio commented May 5, 2018

@pulsejet That's really cool! :D
Although the game feels a bit dull without the Midi music in the background. Is fluidsynth out of the question for emscripten? You could also render the midi tracks to .ogg and replace the midis; if you keep the filename sans extension the same, the game will pick the vorbis oggs instead.

@Ghabry
Copy link

Ghabry commented May 5, 2018

Fluidsynth can't be really used because the soundfonts must be downloaded. Are there any small soundfonts that don't sound bad?
btw, for EasyRPG Player we use "FmMidi". This is a ~300 kbyte dependency and it synthesizes the audio instead of using soundfonts/gus patches. Sounds more electronical/different but at least you get Midi in the web.

@pulsejet
Copy link
Author

pulsejet commented May 5, 2018

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.

@pulsejet
Copy link
Author

Thought I'd mention here, BGM, BGS and ME aren't working since a separate thread is spawned for an AudioStream, unlike a SoundEmitter (hence SE works), and we have no thread support :(. Haven't figured out yet what might be the best workaround to this.

@pulsejet
Copy link
Author

pulsejet commented May 8, 2019

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

@Ghabry
Copy link

Ghabry commented May 8, 2019

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).
And instead of dummy images we have a "index.json" file which contains a file mapping (e.g. "CharSet/actor -> Charset/Actor.png). Maybe a similiar index could be used, plus image dimensions because scripts depend on them.

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

@pulsejet
Copy link
Author

pulsejet commented May 9, 2019

emscripten async fetch + callback Is basicly how we do it in EasyRPG Player (for 2k and 2k3)

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.

And instead of dummy images we have a "index.json" file which contains a file mapping (e.g. "CharSet/actor -> Charset/Actor.png).

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.

@Ghabry
Copy link

Ghabry commented May 9, 2019

Well here for reference our async handler code:
https://github.com/EasyRPG/Player/blob/master/src/async_handler.cpp

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.
But you will still need some extra information for Bitmap width/height, as you can't control when the ruby script will read this.

and our index.json generator:
https://github.com/EasyRPG/Tools/tree/master/gencache

any idea how audiostream (for bgm/bgs/me) could be converted to not using a thread

Unfortunately not, we use the SDL audio API, so have no idea how to implement this.

@Ancurio
Copy link
Owner

Ancurio commented May 10, 2019

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.

From the top of my head: There is one threaded class, ALStream, that is just a low level audio player; it needs to be continually fed sound data. I need it to implement the custom ogg/midi looping of RMXP+ players; if you can replace it with something that is periodically fed OpenAL buffer objects to be queued, you can feed it directly from the ALDataSource subclasses.
Alternatively, the ALStream class could also be rewritten to not run on a thread but do its processing from the "main" thread with a periodic function call. I think same goes for the MEWatch in the Audio class.

@pulsejet
Copy link
Author

pulsejet commented May 7, 2020

@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):

  • Still running mruby, but pretty much everything "just works". Mruby 2.1.0 fixes most of the compatibility issues. With a few mrbgems, it is almost fully compatible with existing scripts (with the exception of begin ... end until, which is the only thing I had to change).
  • mkxp.wasm is just ~1.6mb after gzip!
  • With the new Asyncify, performance is acceptable even if the main loop is in ruby, but I am still using the main_update_loop hack cause it is still better.
  • Image rendering is done by the browser natively (so no libpng or libjpeg needed) using Emscripten's preload plugins.
  • Audio works by updating the buffers through a call on every frame. Only ogg is supported to get rid of the SDL_sound dependency. The only deps in use here are mruby, sigc++, physfs and pixman.
  • All assets are loaded asynchronously (graphics, scripts, audio etc.). A map (like this one is generated with a small bash script that creates dummy files in the memfs on startup, which is then populated in a lazy manner whenever an asset is requested.
  • Save files are persisted to indexeddb by calling a function from ruby.
  • I've a small script that does a rudimentary static analysis on the Map00x.rxdata files and builds a list of assets it refers to. This is used for preloading assets asynchronously when this file is loaded.
  • SDL atomic is broken in emscripten? Removing it fixes performance and strange crashes.

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

@Ancurio
Copy link
Owner

Ancurio commented Aug 13, 2020

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

@pulsejet
Copy link
Author

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)

@pulsejet
Copy link
Author

pulsejet commented Oct 19, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants