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

sokol_app custom canvas name not working #407

Closed
OAguinagalde opened this issue Oct 27, 2020 · 10 comments
Closed

sokol_app custom canvas name not working #407

OAguinagalde opened this issue Oct 27, 2020 · 10 comments
Assignees
Labels

Comments

@OAguinagalde
Copy link

Trying to have multiple canvases and sokol applications running on one same page I started trying some things.
One thing I would need is to change the name of the canvas of each application, which in sokol, defaults to "canvas".
We can change on creation with html5_canvas_name, correct, and if we set a name for a canvas that is not present in the HTML it even warns you that it's not a valid name, that's great too.

However, I haven't been able to successfully set a canvas name and use it. If the canvas is not called canvas, I can't in any way use it, even if my names match.

Setting the name as "aaaa" in my constructor and a canvas like this:

<canvas id="aaaa"/>

sokol_app won't complain that it cant find the canvas (I tested changing the name in my c code and it does indeed complain, so just to make it clear) and it tries to create the context and other initializations, and in function _sapp_emsc_webgl_init it crashes giving me the following on the Chrome debugger:

app.js:7607 exception thrown: TypeError: Cannot read property 'GLctx' of undefined,TypeError: Cannot read property 'GLctx' of undefined
    at _emscripten_webgl_enable_extension (http://127.0.0.1:5500/Docs/demos/1/app.js:5693:25)
    at _sapp_emsc_webgl_init (<anonymous>:wasm-function[175]:0x56d9)
    at _sapp_emsc_run (<anonymous>:wasm-function[170]:0x409f)
    at sapp_run (<anonymous>:wasm-function[169]:0x3cf7)
    at ce_game_run (<anonymous>:wasm-function[453]:0x33097)
    at __original_main (<anonymous>:wasm-function[147]:0x2013)
    at main (<anonymous>:wasm-function[148]:0x202e)
    at http://127.0.0.1:5500/Docs/demos/1/app.js:2002:22
    at callMain (http://127.0.0.1:5500/Docs/demos/1/app.js:7585:15)
    at doRun (http://127.0.0.1:5500/Docs/demos/1/app.js:7648:23)
callMain @ app.js:7607
doRun @ app.js:7648
run @ app.js:7663
runCaller @ app.js:7561
removeRunDependency @ app.js:1919
receiveInstance @ app.js:2065
receiveInstantiatedSource @ app.js:2082
Promise.then (async)
instantiateArrayBuffer @ app.js:2089
(anonymous) @ app.js:2112
Promise.then (async)
(anonymous) @ app.js:2107
Promise.then (async)
instantiateAsync @ app.js:2105
createWasm @ app.js:2132
(anonymous) @ app.js:7265
demo.onload @ cengine.md.html:81
load (async)
loaddemo @ cengine.md.html:79
window.onload @ cengine.md.html:102
load (async)
(anonymous) @ cengine.md.html:100

Looking into what sokol_app is doing:

_SOKOL_PRIVATE void _sapp_emsc_webgl_init(void) {
    EmscriptenWebGLContextAttributes attrs;
    emscripten_webgl_init_context_attributes(&attrs);
    attrs.alpha = _sapp.desc.alpha;
    attrs.depth = true;
    attrs.stencil = true;
    attrs.antialias = _sapp.sample_count > 1;
    attrs.premultipliedAlpha = _sapp.desc.html5_premultiplied_alpha;
    attrs.preserveDrawingBuffer = _sapp.desc.html5_preserve_drawing_buffer;
    attrs.enableExtensionsByDefault = true;
    #if defined(SOKOL_GLES3)
        if (_sapp.desc.gl_force_gles2) {
            attrs.majorVersion = 1;
            _sapp.gles2_fallback = true;
        }
        else {
            attrs.majorVersion = 2;
        }
    #endif
    EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp.html5_canvas_name, &attrs);
    if (!ctx) {
        attrs.majorVersion = 1;
        ctx = emscripten_webgl_create_context(_sapp.html5_canvas_name, &attrs);
        _sapp.gles2_fallback = true;
    }
    emscripten_webgl_make_context_current(ctx);

    /* some WebGL extension are not enabled automatically by emscripten */
    emscripten_webgl_enable_extension(ctx, "WEBKIT_WEBGL_compressed_texture_pvrtc");
}

The crash happens at the end, on the call to emscripten_webgl_enable_extension(ctx, "WEBKIT_WEBGL_compressed_texture_pvrtc"); however I suspect the issue comes from before that, in emscripten_webgl_create_context.

After some testing, when I don't set any canvas name and go with defaults, rendering properly, the value of ctx is 1, which according to the documentation means:

#define EMSCRIPTEN_RESULT int
// This type is used to return the result of most functions in this API. Zero and positive values denote success, while negative values signal failure. Possible values are listed below.
#define EMSCRIPTEN_RESULT_SUCCESS              0
// The operation succeeded.
#define EMSCRIPTEN_RESULT_DEFERRED             1
// The requested operation cannot be completed now for web security reasons, and has been deferred for completion in the next event handler.

The docs also say that the return type of emscripten_webgl_create_context is:

On success, a strictly positive value that represents a handle to the created context. On failure, a negative number that can be cast to an EMSCRIPTEN_RESULT field to get the reason why the context creation failed.

That means that when everything is good it returns 1 in my machine and will do some initializations later on, since it's deferred, that's all good. When trying to use a custom canvas name however, the return I get is 0.

According to the docs, that's not even possible? It's supposed to get an strict positive on success or a negative on failure? Well I get a 0, which also, according to the docs is a success. It's confusing. However if the only change is the canvas name, and it really succeeded I would expect to get a 1, since that's what a success returns when using a default canvas.

Anyway my point is, that it's al weird and that I believe the context creation is failing, but the only difference is that name of the canvas, so I have no clue how to debug this. Hopefully it's just a silly thing I'm doing wrong... Is there any examples of the canvas name being different and working?

Not sure if related but as a side note... If I set the name as default in my .c source, and create 2 canvas like this in my html.

<canvas id="aaaa"/>
<canvas id="canvas"/>

Even though by default it should take the 2nd one, it actually takes the first one... I'm really confused.

@OAguinagalde
Copy link
Author

Ok, Reading further the documentation. The return type of emscripten_webgl_create_context is EMSCRIPTEN_WEBGL_CONTEXT_HANDLE which:

Represents a handle to an Emscripten WebGL context object. The value 0 denotes an invalid/no context (this is a typedef to an int).
So disregard what I was saying in the previous message about EMSCRIPTEN_RESULT, since it doesn't apply.

In my defense I will say that the docs contradict themselves since it also says that the function returns a strict positive (the handle itself) or a negative on failure, and forgets to mention that 0 is algo a failure. Anyway, my problem persist and this only confirms that emscripten_webgl_create_context is failing for some reason, even though the only difference is the canvas name. Is there some kind of expected syntax for the name of the canvas?

@floooh
Copy link
Owner

floooh commented Oct 27, 2020

This could be a bug in sokol_app.h, or something weird in the emscripten SDK's emscripten_webgl_create_context() function, I need to look into this.

But in general, if you have a canvas element in the HTML file like this:

<canvas id="aaaa" ...>

...then the sokol_main() function should look like this:

sapp_desc sokol_main(...) {
    return (sapp_desc){
        .html5_canvas_name = "aaaa",
        ...
    };
}

If this doesn't work then it's either a bug in sokol_app.h or in emscripten_webgl_create_context().

As far as I'm aware, I'm only using a hardwired canvas name in the (still experimental) WebGPU setup code here:

sokol/sokol_app.h

Line 4381 in 345fa62

var gpuContext = document.getElementById("canvas").getContext("gpupresent");

But for WebGL, it should always use the canvas name from the sapp_desc struct (and if this isn't set, the default "canvas").

There are some special "meta-names" for HTML elements in the emscripten APIs (e.g. "#canvas" vs "canvas", but I can't currently find the documentation for those, they might be deprecated (but for this specific use case they would be irrelevant anyway).

@floooh
Copy link
Owner

floooh commented Oct 27, 2020

Ok, I can reproduce the error, if both the canvas name in the HTML and the name passed to emscripten_webgl_create_context() match, but are different than "canvas", then the function returns a 0 context handle (which is a problem, it should return a context handle > 0).

If the canvas name doesn't match, than sokol_app.h already errors out earlier before the WebGL context is created (that's where the "sokol_app.h: invalid target" message is coming from.

I'll look into this.

@floooh floooh self-assigned this Oct 27, 2020
@floooh floooh added the bug label Oct 27, 2020
@OAguinagalde
Copy link
Author

What you are saying sounds exactly like my issue. I tried to dig around and tried updating to latest versions of both emsdk and sokol but it wouldn't work (on my first tries I was using like 3 to 4 months old repos).

I even tried hardcoding in the generated .js file my own canvas element but also would fail, so it kind of seemed an issue in emsdk and sokol seemed to have the correct name all the time.

I'll try to look more into the emsdk side of it or even how sdl2 does it (if at all). Anyway, thanks!

@floooh
Copy link
Owner

floooh commented Oct 27, 2020

I think I found the issue, emscripten_create_webgl_canvas() expects the canvas id in the form "#canvas_name" (the leading # is important).

I will add a fix to sokol_app.h, so that this leading '#' is added internally (just using .html5_canvas_name = "#canvas_name" doesn't work because that name is also used in various places with getElementById(), and this chokes on the leading '#'.

Before that I'll try to find documentation why this is the case or maybe ask around on the emscripten discussion group...

PS: ah, probably that's why:

https://www.w3schools.com/cssref/css_selectors.asp

'#id' is the CSS convention for identifying an element by its id.

This also explains why 'canvas' picks "any" canvas, because this is the convention to select by the HTML element's tag name. I clearly understood this part wrong...

@floooh
Copy link
Owner

floooh commented Oct 27, 2020

I have committed a fix here:

61730df

No changes should be required from your side. The sapp_desc.html5_canvas_name should be set to the canvas id as before. Internally this will use a leading '#' for the emscripten functions which need a CSS selector instead of an id.

Please let me know if it works so I can close the ticket :)

@floooh
Copy link
Owner

floooh commented Oct 27, 2020

...I wonder if you will run into followup problems when running multiple sokol_app.h instances and WebGL canvases on the same page because of things like this:

sokol/sokol_app.h

Line 3774 in 030e901

Module.sapp_emsc_target = document.getElementById(target_str);

...I'm not sure if Module is a "global" page-wide object, or per WASM instance.

Let me know how it goes :)

@OAguinagalde
Copy link
Author

Aah, I should have looked more into it, I did see some examples of emscripten using the '#' in the names but didn't find anything talking about it so didn't look into it.

It's working great now! As you said I also expected there to be issues with the Module but emscripten has a pretty good explanation of how to work with it here and more specifically the solution I'm using is explained here, using the MODULARIZE build option, but I'll copy it here for reference:


Another option is to use the MODULARIZE option, using -s MODULARIZE=1. That puts all of the generated JavaScript into a factory function, which you can call to create an instance of your module. The factory function returns a Promise that resolves with the module instance. The promise is resolved once it’s safe to call the compiled code, i.e. after the compiled code has been downloaded and instantiated. For example, if you build with -s MODULARIZE=1 -s 'EXPORT_NAME="createMyModule"', then you can do this:

createMyModule(/* optional default settings */).then(function(Module) {
    // this is reached when everything is ready, and you can call methods on Module
});

In my case for example, I want to have a page with multiple examples for a project of mine, so I have a folder structure that looks like this:

index.html
demos (folder)
    ├───1 (folder)
    └───2 (folder)
    └───3 (folder)

Each numbered folder will have the .js, .data and .wasm for each demo (which are a single c source file). After building using the flags -s MODULARIZE=1 -s 'EXPORT_NAME="DemoModule"' then in my html:

Here is my first demo...
<canvas class="game" id="demo1" height="100" width="700"/>
And another demo here!
<canvas class="game" id="demo2" height="100" width="700"/>
<script>
// Load the numbered demo by creating a script element with the source
//  being the one generated by emscripten. When the script is loaded, start
//  the "execution" of the demo itself.
var loaddemo = function (number) {
    const demo = document.createElement('script');
    demo.src = 'demos/' + number + '/' + 'app.js';
    demo.type = 'text/javascript';
    demo.onload = function(url) {
        // This is "DemoModule" because that's how I have defined it when building with emscripten:
        //   ... -s MODULARIZE=1 -s 'EXPORT_NAME="DemoModule"'
        DemoModule({
            // Here it's possible to overwrite part of Modules if needed before execution.
            // Since the wasm and data files are not in the same location as my .html file,
            //  I change the Module.locateFile function to return the proper folder it should use.
            'locateFile' : function(url) {
                console.log("file located: " + url + '\n' + 'demos/' + number + '/' + url);
                return 'demos/' + number + '/' + url;
            }
        }).then(function(Module) {
            // This is called after the Module has been loaded if I understand correctly
            console.log('Loaded demo ' + number);
        });
    };
    document.head.appendChild(demo);
}

// When the page loads, just load the demos...
window.onload = function () {
    console.log("Page loaded");
    loaddemo(1);
    loaddemo(2);
}
</script>

Ofcourse, in the .c source code of the first demo I've set the sokol canvas name to demo1 and in the second one demo2.
I haven't tried to do anything crazy so far, but seems to work great for now!

image

I don't have a public showcase of it now but I might soon, I'll let you know :)
Thanks for the fast replies and fixes!

@OAguinagalde
Copy link
Author

OAguinagalde commented Oct 28, 2020

Just an extra detail in case someone might want to try this. I'm not sure why, or if it's an issue or a bug, but if I set the html5_canvas_resize to false and resize the window, the canvas get's bigger every time I resize the browser:

image

In that image, the first application's canvas_resize is set to false and the second one's is set to true. And I have resized the window a couple of times, the more I do it, the bigger the canvas gets. It might also be something in my configuration on my css or something:

This is my canvas:

<canvas class="game" id="demo1" height="100" width="700"/>

And this is my game css class:

.game {
  border: black solid 2px;
  image-rendering: optimizeSpeed;
  image-rendering: -moz-crisp-edges;
  image-rendering: -o-crisp-edges;
  image-rendering: -webkit-optimize-contrast;
  image-rendering: optimize-contrast;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
  -ms-interpolation-mode: nearest-neighbor;
}

There is other stuff but not sure what it could be causing this.

@OAguinagalde
Copy link
Author

Aparently it was the border... I guess that when querying the size of the canvas, it was returning canvas size + the border, which was 2px. Then setting that size again to the canvas, meaning that every frame it would grow 4 pixels, which I confirmed trying to make my window 1 pixel larger would make the canvas 4 pixels larger. Seems to make sense. probably a bug. Doesn't affect me but since I already mentioned it I will write my discoveries (completely not relevant to the issue) here.

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

No branches or pull requests

2 participants