3

Emscripten/HTML integration tips

 3 years ago
source link: https://floooh.github.io/2017/02/22/emsc-html.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Emscripten/HTML integration tips

Feb 22, 2017

TL;DR things I learned while adding a HTML UI to my 8-bit emu

Update 25-Feb-2017: a small update to the last section with an easier way to mark C-functions which should be callable from Javascript

I recently spent a weekend and a few evenings to make my YAKC emulator appear more like a proper web application instead of looking like a, well… Java applet.

This is what I did:

  1. stretch the WebGL canvas over the whole browser window client area
  2. add a (very simple) HMTL/CSS UI on top
  3. create a C function interface so that the webpage can call into the emulator

The actual changes are small, but the ‘psychological effect’ is pretty big IMHO, see for yourself: http://floooh.github.io/virtualkc/.

This is what the webpage looked like before:

yakc-old

Here’s how it works:

Soft Fullscreen

Emscripten provides a fairly extensive fullscreen API, which on one hand provides a wrapper around the HTML5 Fullscreen API, and on the other hand also provides a ‘soft fullscreen’ mode, which stretches a WebGL canvas over the available browser window client area and takes care of all the details.

First, let’s discard the HTML5 Fullscreen API, it sounds great at first since it provides a fullscreen canvas without any browser window chrome. In reality it is quite useless however, since fullscreen mode must be activated from an input handler, and even then a warning is printed which has a different look and feel on each browser. The kicker is though that the Fullscreen API isn’t supported on iOS Safari (last I checked).

Emscripten’s soft fullscreen mode is much more useful, it can be activated outside an input event handler, doesn’t show a warning, and works on all browsers. It only has one downside: when activated, it hides all other HTML elements on the page, so it can’t be used together with an HTML UI which should hover on top of the WebGL canvas.

In the end I added a new fullscreen mode behaviour to Oryol which works by passively tracking the size of an HTML element (in this case: the WebGL canvas which the emulator renders to), so basically: ‘use whatever current size this HTML element has for the WebGL default framebuffer’.

This ‘tracked fullscreen mode’ is activated by setting a flag in the GfxSetup descriptor object and optionally setting the name (DOM id) of an HTML element:

auto gfxSetup = GfxSetup::Window(width, height, "YAKC Emulator");
gfxSetup.HtmlTrackElementSize = true;
gfxSetup.HtmlElement = "canvas";
Gfx::Setup(gfxSetup);

The HTML main page houses the WebGL canvas:

<canvas class="game" id="canvas" oncontextmenu="event.preventDefault()"></canvas>

The CSS for the canvas takes care of stretching the canvas over the available space:

.game {
    position: absolute;
    top: 0px;
    left: 0px;
    margin: 0px;
    border: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    display: block;
}

On the C++ side, display management code needs to know the initial size of the WebGL canvas, and it needs to keep track of size changes (usually caused by the user resizing the window):

Emscripten has the function emscripten_get_element_css_size() to query the width and height of a named HTML element. I’m calling this first to get the initial size of the canvas DOM element, and then call emscripten_set_canvas_size() to initialize the framebuffer size of the canvas to the same size as its DOM element.

Finally I’m setting up a callback to keep track of window size changes.

The canvas initialization code on the C++ side now looks like this:

double width, height;
emscripten_get_element_css_size(renderSetup.HtmlElement.AsCStr(), &width, &height);
emscripten_set_canvas_size(int(width), int(height));
emscripten_set_resize_callback(nullptr, nullptr, false, emscWindowSizeChanged); 

Inside the resize-callback, the same procedure repeats, first query the new size of the canvas DOM element, then update the framebuffer size of the canvas. Without this, the canvas content would simply be scaled to the new DOM element size:

That’s all there is to stretching the WebGL canvas over the entire browser window client area and have the canvas framebuffer automatically resize whenever the window size changes.

HTML/CSS Overlay UI

For the overlay UI I was at first considering a CSS framework, but after I looked at a few of them I realized that those frameworks are often just good for building specific webpage types where the entire layout is controlled by the framework components, but don’t fit the situation well where the UI needs to float on top of an element that takes up the entire background. Maybe there are such CSS frameworks, but in the end I stopped looking and just wrote the little required CSS myself since I just needed a menu, some panels and buttons.

BTW: The ‘one clever trick that blew my mind’ I learned from looking at CSS frameworks was that a hamburger menu icon is just a few divs:

<div class="nav-btn" onclick="nav_toggle()">
    <div class="nav-btn-bar"></div>
    <div class="nav-btn-bar"></div>
    <div class="nav-btn-bar"></div>
</div>

The CSS turns those 3 button-bars into rectangles with rounded corners, and voilá, there’s your hamburger icon :D

The one important thing for the whole overlay UI is the use of the CSS attribute ‘z-index: 1’ to make it appear in front of the background WebGL canvas. Using z-index also implicates the ‘position’ attribute, and for visible panels and buttons ‘display: block’ or ‘display: inline-block’.

For instance this is what the CSS for a button div looks like (or rather SASS, note the $variables):

.panel-button {
    background-color: $ui-button-color;
    display: inline-block;
    cursor: pointer;
    padding: 5px;
    margin: 2px;
    border-radius: 3px;
    font-family: "Arial", Gadget, sans-serif;
    font-size: 12px;    
    color: $ui-text-color;
    text-align: center;
}

Giving buttons like this:

yakc-btn

In the end, the whole HTML UI is just a bunch of nested divs, a few simple CSS rules and Javascript onclick handlers.

Javascript/emscripten interaction

The new HTML UI needs to talk to the emulator’s cross-compiled asm.js code. This happens through a small set of Javascript functions which call extern “C” functions on the emscripten side, and these Javascript functions are called from onclick handlers of DOM elements, but you can just as well call them manually from the JS console, for instance:

This should boot into the Amstrad CPC 6128. The emulator essentially offers a C function interface callable from the Javascript side.

Here’s how it works:

On the C++ side, the whole C function interface is inside an extern “C” block and all the C functions in there have simple argument and return types (no structs, or pointers to structs. For instance the yakc_boot() function looks like this:

extern "C" {

void yakc_boot(const char* sys_str, const char* os_str) {
    auto* app = YakcApp::self;
    if (app) {
        system sys = system_from_string(sys_str);
        os_rom os = os_from_string(os_str);
        if (system::none != sys) {
            app->emu.poweroff();
            app->emu.poweron(sys, os);
        }
    }
}

}

On the Javascript side there’s a similar function which uses an emscripten helper Module.ccall() to invoke the ‘C-side function’ (in the end, the C function is also just a Javascript function of course, but Module.ccall() takes care of marshalling the function arguments):

function yakc_boot(sys, os) {
    Module.ccall('yakc_boot', null, ['string','string'], [sys, os]);
}

Theoretically that’s it. But when compiling the C/C++ code, the emscripten toolchain will either remove the C function completely (since it thinks it is dead code), or it will minify the function name so that the JS code can’t find it.

Update 25-Feb-2017: To make C functions visible to Javascript the best solution is to use the EMSCRIPTEN_KEEPALIVE attribute:

extern "C" {

EMSCRIPTEN_KEEPALIVE void 
yakc_boot(const char* sys_str, const char* os_str) {
    ...
}

}

This has the same effect as the EXPORTED_FUNCTIONS linker command line parameter, but doesn’t leak into the build system. There will soon be an alias for EMSCRIPTEN_KEEPALIVE with a better name: EMSCRIPTEN_EXPORT.

this is the original version using -s EXPORTED_FUNCTIONS: To avoid this, emscripten needs to know the names of all C functions which should be ‘exported’ through a linker-stage command line parameter called EXPORTED_FUNCTIONS which takes a Python/JS-style string array:

> emcc ... -s EXPORTED_FUNCTIONS=['_main','_yakc_boot']

Note the underscore (that’s how C function names show up to the linker), and that the main() function must be part of the list (otherwise the emscripten linker will kill the main function as dead code).

In my own projects I’m using the fips build system (basically a bunch of python helper scripts around cmake), where I’m putting the list of exported functions into a project-local build config YAML file, for instance:

---
platform: emscripten 
generator: Unix Makefiles
build_tool: make
build_type: Release
cmake-toolchain: emscripten.toolchain.cmake
defines:
    FIPS_NO_ASSERTS_IN_RELEASE: ON
    FIPS_EMSCRIPTEN_MEM_INIT_METHOD: 0
    FIPS_EMSCRIPTEN_EXPORTED_FUNCTIONS: [
        _main,
        _yakc_boot,_yakc_toggle_ui,_yakc_toggle_keyboard,
        _yakc_toggle_joystick,_yakc_power,_yakc_reset,
        _yakc_get_system,_yakc_quickload,_yakc_toggle_crt,
        _yakc_loadfile
    ]

…and that’s all the secret wisdom needed to turn your emscripten application from a 90’s Java-applet-lookalike (yuck!) into a proper 21st-century hipster-certified material-design extravaganza. And you don’t even need to use a Javascript framework :)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK