Skip to content

Multithreading 34/34: OffscreenCanvas + proxied Canvas support#6254

Merged
dschuff merged 1 commit intoemscripten-core:incomingfrom
juj:offscreencanvas_and_proxied_canvas
Nov 14, 2018
Merged

Multithreading 34/34: OffscreenCanvas + proxied Canvas support#6254
dschuff merged 1 commit intoemscripten-core:incomingfrom
juj:offscreencanvas_and_proxied_canvas

Conversation

@juj
Copy link
Collaborator

@juj juj commented Feb 16, 2018

This PR improves supports for OffscreenCanvas, allowing pthreads to resize canvases from threads, and also creating proxied canvases on the main thread when OffscreenCanvas is not present.

Proxying GL has the benefit of enabling support for eglMakeCurrent(), for true multithreaded WebGL access, i.e. threads can acquire and release access to a single GL context.

Additionally this PR optimizes the proxied GL calls to run asynchronously whenever possible, to avoid synchronous blocking. For example UE4 runs in general without having to block to wait for sync GL calls at all.

Proxying naturally has a performance impact compared to native OffscreenCanvas. That is why proxying is optional, and codebases explicitly need to enable it via OFFSCREEN_FRAMEBUFFER flag when desired.

Codebases will want to choose whether to do true OffscreenCanvas, whether to proxy, or whether to do both, with proxying as a fallback to OffscreenCanvas, so this PR follows an "all options open" type of approach so that codebases can experiment, to find bugs and gauge performance.

This is the last PR of the Multithreading series, and also the largest, given that the WebGL proxying part touches all GL functions. This should be looked at very last after all of the other Multithreading PRs have landed.

Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments and questions, but I have not read this all yet - I need feedback from you to fully understand the approach here.

Overall I think an overview of the approach should be documented somewhere - perhaps in settings.js alongside the relevant options, or in library_gl.js at the top?


var LibraryGL = {
#if USE_PTHREADS
// If GLctxIsOnParentThread is true, then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about GLctxIsOnParentThread => proxiedGLctx? That seems more self-explanatory I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GLctxIsOnParentThread is more self-explanatory to suggest the type is a boolean, whereas proxiedGLctx could be mistaken with GLctx to be some kind of object, but just a proxied flavor, instead of the real thing. I'll change to GLctxIsProxied to hint at the boolean nature?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

var handle = _malloc(8); // Make space on the heap to store GL context attributes that need to be accessible as shared between threads.
{{{ makeSetValue('handle', 0, 'webGLContextAttributes["explicitSwapControl"]', 'i32')}}}; // explicitSwapControl
#if USE_PTHREADS
{{{ makeSetValue('handle', 4, '_pthread_self()', 'i32')}}}; // the thread pointer of the thread that owns the control of the context
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "own control of the context" mean?

A place with a general overview of how the proxying works would be good I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"own control of the context" means that the JavaScript WebGLRenderingContext resides in that thread. Adjusted the comment.

$emscripten_get_canvas_element_size_js: function(target) {
var stackTop = Runtime.stackSave();
var w = Runtime.stackAlloc(8);
var h = w + 4;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will not work on the LLVM wasm backend, the stack grows down there.

h = (w + stackTop) / 2 should work ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely stackAlloc() will still return a pointer to the beginning of the allocated memory address on the stack? So if one calls var p = stackAlloc(8), then HEAPU8[p] through HEAPU8[p+7] are the bytes that were allocated?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, you add to the beginning of the allocation. That seems fine then.

var w = Runtime.stackAlloc(8);
var h = w + 4;

if (typeof target === 'string') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this path necessary? if called from JS, the caller can just convert the string to an int..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would not want to give the caller the burden to have to allocate and freestrings on the heap - it is smaller code wise to convert here inside (1 call site), rather than requiring all callers to convert (N call sites). User might also accidentally malloc, running into perf issues compared to stackAlloc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, ok.

var newWidth = (cssWidth * dpiScale)|0;
var newHeight = (cssHeight * dpiScale)|0;

if (!target.controlTransferredOffscreen) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern (if not control transferred, apply, othrewise do the call) appears 3 times. can the logic be moved into the call, that is, emscripten_set_canvas_element_size_js would check if the target has controlTransferredOffscreen and handle that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good point. Changed this, and while doing that, I realize that I am not actually ready to expose this function as a "public" function to JavaScript side, because that is something that will need a closer thought as part of the big elephant issue we have with the JSEvents object being a non-DCEable bloat (#2999, #3298). I think we need to remove that whole object and make all functions individually DCE well (even when Closure is not used), and that will require changes to how users expose the functions to JS code, if they do not call them from C++.

So for now changed the canvas set and get functions to be underscored internal functions - the above refactor would publicize them in a proper manner, while doing the same for other functions.

@@ -1689,7 +1689,6 @@ void* emscripten_GetProcAddress(const char *name_) {
if (!strcmp(name, "glNormalPointer")) return emscripten_glNormalPointer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have some worry about how this stuff will work with the GetProcAddress logic - it would be good to make sure all our GPA tests also run in this proxied mode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR only moves this file around for organizational purposes. The intent of obtaining function pointers to JS functions remains same - pointer to JS function is returned on the calling thread, and the called GL function then proxies if needed.

@kripken
Copy link
Member

kripken commented Oct 31, 2018

Noticed while looking at the code with @kainino0x - some of the new tests have just cpp files, but no python setup to run them, specifically

  • gl_in_two_pthreads.cpp
  • resize_offscreencanvas_from_main_thread.cpp

I verified it's just those two.

@kainino0x
Copy link
Collaborator

kainino0x commented Nov 1, 2018

Having issues with a basic SDL test program against juj:multithreading_october_2018_old_sockets, which contains this patch. It's also based on 1.38.11, so could be out-of-date.

  • screen.width/screen.height fails, and if you work around that:
  • setWindowTitle fails, and if you work around that:
  • SDL_GL_CreateContext calls eglCreateContext, which calls glutCreateWindow, which calls createContext with Module['canvas'], which is undefined.
  • also, registerFocusEventCallback tries to run on the proxied main and fails
// WORKS:  emcc -s WASM=1 -s USE_SDL=2
// BROKEN: emcc -s WASM=1 -s USE_SDL=2 -s USE_PTHREADS=1 -s PROXY_TO_PTHREAD=1

#include <assert.h>
#include <stdio.h>

#include <SDL.h>
#include <SDL_opengl.h>

int main() {
  printf("main\n");

  assert(0 == SDL_Init(SDL_INIT_VIDEO|SDL_INIT_EVENTS));
  SDL_Window *window = SDL_CreateWindow("title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 512, 512, SDL_WINDOW_OPENGL);
  assert(window);
  SDL_GLContext gl_context = SDL_GL_CreateContext(window);
  assert(gl_context);

  assert(0 == SDL_GL_MakeCurrent(window, gl_context));

  printf("main VENDOR: %s\n", glGetString(GL_VENDOR));
  glClearColor(0, 1, 0, 1);
  glClear(GL_COLOR_BUFFER_BIT);
  SDL_GL_SwapWindow(window);

  printf("main exit\n");
}

@juj
Copy link
Collaborator Author

juj commented Nov 1, 2018

I never had a chance to look to bring SDL2 library over to multithreading world, and reading the description above, the bit SDL_GL_CreateContext calls eglCreateContext reads like something that will cause issues, because current JS-implemented EGL (library_egl.js) is not multithreading aware, and probably not even proxying aware. PR #5580 looks to make EGL fully multithreading aware, but that one is not ready yet. I would expect SDL2 will need some amount of adjustment after that PR to make things flow smooth. If you did have a path that can avoid SDL2, that will likely be of less resistance (or another approach may be to work towards making SDL2+EGL multithreading-enabled first)

@juj juj force-pushed the offscreencanvas_and_proxied_canvas branch 6 times, most recently from 9dda33c to 0bdba72 Compare November 9, 2018 14:38
@juj
Copy link
Collaborator Author

juj commented Nov 9, 2018

Some initial comments and questions, but I have not read this all yet - I need feedback from you to fully understand the approach here.

Overall I think an overview of the approach should be documented somewhere - perhaps in settings.js alongside the relevant options, or in library_gl.js at the top?

Added documentation to https://github.com/kripken/emscripten/pull/6254/files#diff-16eca2b70acff1fd624285ab9390a9c7

@juj juj force-pushed the offscreencanvas_and_proxied_canvas branch 2 times, most recently from fd51a60 to 2908e26 Compare November 9, 2018 14:43
@juj
Copy link
Collaborator Author

juj commented Nov 9, 2018

Noticed while looking at the code with @kainino0x - some of the new tests have just cpp files, but no python setup to run them, specifically

* `gl_in_two_pthreads.cpp`

* `resize_offscreencanvas_from_main_thread.cpp`

I verified it's just those two.

Good catch, this was result from first having authored all the multithreading work in one big branch, and then later splitting it up in different PRs, so test code for those two were lost in the change. Added tests for these now.

Rebased against latest incoming, this should now be good.

@juj juj force-pushed the offscreencanvas_and_proxied_canvas branch 3 times, most recently from 2d1e9fe to 9663ba2 Compare November 12, 2018 15:45

If a WebGL rendering context is created as an OffscreenCanvas-based context, it will have the limitation that only the pthread that created the context can enable access to it (via ``emscripten_webgl_make_context_current()`` function). Other threads will not be able to activate rendering to the context, i.e. OffscreenCanvas-based contexts are essentially "pinned" to the pthread that created them.

If the current browser does not support OffscreenCanvas, you can specify the ``EMSCRIPTEN_WEBGL_CONTEXT_PROXY_ALWAYS`` WebGL context creation flag. If this flag is passed, and code was compiled with ``-s OFFSCREEN_FRAMEBUFFER=1`` enabled, the WebGL context will be created as a "proxied context". In this context mode, the WebGLRenderingContext object will actually be created on the main browser thread, and all WebGL API calls will be proxied as asynchronous messages from the pthread into the main thread. This will have a performance and latency impact in comparison to OffscreenCanvas contexts, however unlike OffscreenCanvas-based contexts, proxied contexts can be shared across any number of pthreads: you can use the ``emscripten_webgl_make_context_current()`` function in any pthread to activate and deactivate access to the WebGL context: for example, you could have one WebGL loading thread, and another WebGL rendering thread that coordinate shared access to the WebGL rendering context by cooperatively acquiring and releasing access to the WebGL rendering context via the ``emscripten_webgl_make_context_current()`` function. Proxied contexts do not require any special support from the browser, but any WebGL capable browser can create a proxied WebGL context.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final "but" (in "but any WebGL capable browser..") seems like it should be "so"? Or did I misunderstand the intention?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that's much better. The second statement in the sentence is a continuation to the first, not a contrary.

// If GLctxIsProxied is true, then
// a) the running thread is not the main browser thread, and
// b) the currently active GL context is running on the main browser thread, so all GL calls need to be proxied to it.
$GL__postset: 'var GLctx; var GLctxIsProxied = false; GL.init()',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have GLctxIsProxied instead of GL.ctxIsProxied (i.e. as a property on GL)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is good point - at some point I had part of the proxying extend to src/pthread-main.js, which is currently outside minification, so it did not see the GL variable to access, but looking at it now, I no longer have that, so this no longer needs to be global. Given that we have GL.currentContext, I also renamed this once more to GL.currentContextIsProxied to make it look similar.

@kripken
Copy link
Member

kripken commented Nov 12, 2018

Thanks, great updates!

I had a few minor questions and comments now, but aside from those I think this is good to land.

@juj juj force-pushed the offscreencanvas_and_proxied_canvas branch from 9663ba2 to 174cc88 Compare November 14, 2018 09:30
This PR improves supports for OffscreenCanvas, allowing pthreads to resize canvases from threads, and also creating proxied canvases on the main thread when OffscreenCanvas is not present. Bump version to 1.38.19.
@juj juj force-pushed the offscreencanvas_and_proxied_canvas branch from 174cc88 to ee5f632 Compare November 14, 2018 13:07
Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!

@dschuff
Copy link
Member

dschuff commented Nov 14, 2018

Since this includes a version bump, I'm going to go ahead and merge it so we can update the other tags and get CircleCI and the integration waterfall green again.

@dschuff dschuff merged commit 28a144c into emscripten-core:incoming Nov 14, 2018
Beuc pushed a commit to Beuc/emscripten that referenced this pull request Nov 17, 2018
This PR improves supports for OffscreenCanvas, allowing pthreads to resize canvases from threads, and also creating proxied canvases on the main thread when OffscreenCanvas is not present.

Proxying GL has the benefit of enabling support for eglMakeCurrent(), for true multithreaded WebGL access, i.e. threads can acquire and release access to a single GL context.

Additionally this PR optimizes the proxied GL calls to run asynchronously whenever possible, to avoid synchronous blocking. For example UE4 runs in general without having to block to wait for sync GL calls at all.

Proxying naturally has a performance impact compared to native OffscreenCanvas. That is why proxying is optional, and codebases explicitly need to enable it via OFFSCREEN_FRAMEBUFFER flag when desired.

Codebases will want to choose whether to do true OffscreenCanvas, whether to proxy, or whether to do both, with proxying as a fallback to OffscreenCanvas, so this PR follows an "all options open" type of approach so that codebases can experiment, to find bugs and gauge performance.

This is the last PR of the Multithreading series, and also the largest, given that the WebGL proxying part touches all GL functions. This should be looked at very last after all of the other Multithreading PRs have landed.
sbc100 added a commit to sbc100/emscripten that referenced this pull request Jan 29, 2024
The logic here for proxying this call seems to exist in both the JS
code and in the native code.   It seems to have been that way since it
was added in emscripten-core#6254.
sbc100 added a commit to sbc100/emscripten that referenced this pull request Jan 3, 2025
The logic here for proxying this call seems to exist in both the JS
code and in the native code.   It seems to have been that way since it
was added in emscripten-core#6254.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants