diff --git a/src/library_gl.js b/src/library_gl.js index 6ef7a2c3b3397..16bfabbe4edf1 100644 --- a/src/library_gl.js +++ b/src/library_gl.js @@ -497,6 +497,12 @@ var LibraryGL = { webGLContextAttributes['minorVersion'] = 0; } +#if OFFSCREEN_FRAMEBUFFER + // In proxied operation mode, rAF()/setTimeout() functions do not delimit frame boundaries, so can't have WebGL implementation + // try to detect when it's ok to discard contents of the rendered backbuffer. + if (webGLContextAttributes['renderViaOffscreenBackBuffer']) webGLContextAttributes['preserveDrawingBuffer'] = true; +#endif + #if GL_TESTING webGLContextAttributes['preserveDrawingBuffer'] = true; #endif @@ -542,6 +548,146 @@ var LibraryGL = { return context; }, +#if OFFSCREEN_FRAMEBUFFER + // If WebGL is being proxied from a pthread to the main thread, we can't directly render to the WebGL default back buffer + // because of WebGL's implicit swap behavior. Therefore in such modes, create an offscreen render target surface to + // which rendering is performed to, and finally flipped to the main screen. + createOffscreenFramebuffer: function(context) { + var gl = context.GLctx; + + // Create FBO + var fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + context.defaultFbo = fbo; + + // Create render targets to the FBO + context.defaultColorTarget = gl.createTexture(); + context.defaultDepthTarget = gl.createRenderbuffer(); + GL.resizeOffscreenFramebuffer(context); // Size them up correctly (use the same mechanism when resizing on demand) + + gl.bindTexture(gl.TEXTURE_2D, context.defaultColorTarget); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, context.defaultColorTarget, 0); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Create depth render target to the FBO + var depthTarget = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, context.defaultDepthTarget); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, gl.canvas.width, gl.canvas.height); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, context.defaultDepthTarget); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + + // Create blitter + var vertices = [ + -1, -1, + -1, 1, + 1, -1, + 1, 1 + ]; + var vb = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vb); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + context.blitVB = vb; + + var vsCode = + 'attribute vec2 pos;' + + 'varying lowp vec2 tex;' + + 'void main() { tex = pos * 0.5 + vec2(0.5,0.5); gl_Position = vec4(pos, 0.0, 1.0); }'; + var vs = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vs, vsCode); + gl.compileShader(vs); + + var fsCode = + 'varying lowp vec2 tex;' + + 'uniform sampler2D sampler;' + + 'void main() { gl_FragColor = texture2D(sampler, tex); }'; + var fs = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fs, fsCode); + gl.compileShader(fs); + + var blitProgram = gl.createProgram(); + gl.attachShader(blitProgram, vs); + gl.attachShader(blitProgram, fs); + gl.linkProgram(blitProgram); + context.blitProgram = blitProgram; + context.blitPosLoc = gl.getAttribLocation(blitProgram, "pos"); + gl.useProgram(blitProgram); + gl.uniform1i(gl.getUniformLocation(blitProgram, "sampler"), 0); + gl.useProgram(null); + }, + + resizeOffscreenFramebuffer: function(context) { + var gl = context.GLctx; + + // Resize color buffer + if (context.defaultColorTarget) { + var prevTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D); + gl.bindTexture(gl.TEXTURE_2D, context.defaultColorTarget); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.bindTexture(gl.TEXTURE_2D, prevTextureBinding); + } + + // Resize depth buffer + if (context.defaultDepthTarget) { + var prevRenderBufferBinding = gl.getParameter(gl.RENDERBUFFER_BINDING); + gl.bindRenderbuffer(gl.RENDERBUFFER, context.defaultDepthTarget); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, gl.drawingBufferWidth, gl.drawingBufferHeight); // TODO: Read context creation parameters for what type of depth and stencil to use + gl.bindRenderbuffer(gl.RENDERBUFFER, prevRenderBufferBinding); + } + }, + + // Renders the contents of the offscreen render target onto the visible screen. + blitOffscreenFramebuffer: function(context) { + var gl = context.GLctx; + + var prevFbo = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + var prevProgram = gl.getParameter(gl.CURRENT_PROGRAM); + gl.useProgram(context.blitProgram); + + var prevVB = gl.getParameter(gl.ARRAY_BUFFER_BINDING); + gl.bindBuffer(gl.ARRAY_BUFFER, context.blitVB); + + gl.vertexAttribPointer(context.blitPosLoc, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(context.blitPosLoc); + + var prevActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE); + gl.activeTexture(gl.TEXTURE0); + + var prevTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D); + gl.bindTexture(gl.TEXTURE_2D, context.defaultColorTarget); + + var prevBlend = gl.getParameter(gl.BLEND); + if (prevBlend) gl.disable(gl.BLEND); + + var prevCullFace = gl.getParameter(gl.CULL_FACE); + if (prevCullFace) gl.disable(gl.CULL_FACE); + + var prevDepthTest = gl.getParameter(gl.DEPTH_TEST); + if (prevDepthTest) gl.disable(gl.DEPTH_TEST); + + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + if (prevDepthTest) gl.enable(gl.DEPTH_TEST); + if (prevCullFace) gl.enable(gl.CULL_FACE); + if (prevBlend) gl.enable(gl.BLEND); + + gl.bindTexture(gl.TEXTURE_2D, prevTextureBinding); + gl.activeTexture(prevActiveTexture); + // prevEnableVertexAttribArray? + // prevVertexAttribPointer? + gl.bindBuffer(gl.ARRAY_BUFFER, prevVB); + gl.useProgram(prevProgram); + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFbo); + }, +#endif + registerContext: function(ctx, webGLContextAttributes) { var handle = GL.getNewId(GL.contexts); var context = { @@ -567,6 +713,18 @@ var LibraryGL = { if (typeof webGLContextAttributes['enableExtensionsByDefault'] === 'undefined' || webGLContextAttributes['enableExtensionsByDefault']) { GL.initExtensions(context); } + + if (webGLContextAttributes['renderViaOffscreenBackBuffer']) { +#if OFFSCREEN_FRAMEBUFFER + GL.createOffscreenFramebuffer(context); +#else +#if GL_DEBUG + Module.printErr('renderViaOffscreenBackBuffer=true specified in WebGL context creation attributes, pass linker flag -s OFFSCREEN_FRAMEBUFFER=1 to enable support!'); +#endif + return 0; +#endif + } + return handle; }, @@ -3821,7 +3979,16 @@ var LibraryGL = { #if GL_ASSERTIONS GL.validateGLObjectID(GL.framebuffers, framebuffer, 'glBindFramebuffer', 'framebuffer'); #endif + +#if OFFSCREEN_FRAMEBUFFER + // defaultFbo may not be present if 'renderViaOffscreenBackBuffer' was not enabled during context creation time, + // i.e. setting -s OFFSCREEN_FRAMEBUFFER=1 at compilation time does not yet mandate that offscreen back buffer + // is being used, but that is ultimately decided at context creation time. + GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers[framebuffer] : GLctx.canvas.GLctxObject.defaultFbo); +#else GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers[framebuffer] : null); +#endif + }, glGenFramebuffers__sig: 'vii', diff --git a/src/library_glfw.js b/src/library_glfw.js index a2da92565c187..683e902d20577 100644 --- a/src/library_glfw.js +++ b/src/library_glfw.js @@ -996,6 +996,11 @@ var LibraryGLFW = { stencil: (GLFW.hints[0x00021006] > 0), // GLFW_STENCIL_BITS alpha: (GLFW.hints[0x00021004] > 0) // GLFW_ALPHA_BITS } +#if OFFSCREEN_FRAMEBUFFER + // TODO: Make GLFW explicitly aware of whether it is being proxied or not, and set these to true only when proxying is being performed. + contextAttributes.renderViaOffscreenBackBuffer = true; + contextAttributes.preserveDrawingBuffer = true; +#endif Module.ctx = Browser.createContext(Module['canvas'], true, true, contextAttributes); } diff --git a/src/library_glut.js b/src/library_glut.js index ed717eea832d6..260665cc433e5 100644 --- a/src/library_glut.js +++ b/src/library_glut.js @@ -564,6 +564,11 @@ var LibraryGLUT = { stencil: ((GLUT.initDisplayMode & 0x0020 /*GLUT_STENCIL*/) != 0), alpha: ((GLUT.initDisplayMode & 0x0008 /*GLUT_ALPHA*/) != 0) }; +#if OFFSCREEN_FRAMEBUFFER + // TODO: Make glutCreateWindow explicitly aware of whether it is being proxied or not, and set these to true only when proxying is being performed. + contextAttributes.renderViaOffscreenBackBuffer = true; + contextAttributes.preserveDrawingBuffer = true; +#endif Module.ctx = Browser.createContext(Module['canvas'], true, true, contextAttributes); return Module.ctx ? 1 /* a new GLUT window ID for the created context */ : 0 /* failure */; }, diff --git a/src/library_html5.js b/src/library_html5.js index 9509a7280e42c..12b3442b811be 100644 --- a/src/library_html5.js +++ b/src/library_html5.js @@ -1831,12 +1831,21 @@ var LibraryJSEvents = { #if OFFSCREENCANVAS_SUPPORT if (contextAttributes['explicitSwapControl']) { var supportsOffscreenCanvas = canvas.transferControlToOffscreen || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas); + if (!supportsOffscreenCanvas) { +#if OFFSCREEN_FRAMEBUFFER + if (!contextAttributes['renderViaOffscreenBackBuffer']) { + contextAttributes['renderViaOffscreenBackBuffer'] = true; + console.error('emscripten_webgl_create_context: Performance warning, OffscreenCanvas is not supported but explicitSwapControl was requested, so force-enabling renderViaOffscreenBackBuffer=true to allow explicit swapping!'); + } +#else #if GL_DEBUG - console.error('emscripten_webgl_create_context failed: OffscreenCanvas is not supported!'); + console.error('emscripten_webgl_create_context failed: OffscreenCanvas is not supported but explicitSwapControl was requested!'); #endif return 0; +#endif } + if (canvas.transferControlToOffscreen) { GL.offscreenCanvases[canvas.id] = canvas.transferControlToOffscreen(); GL.offscreenCanvases[canvas.id].id = canvas.id; @@ -1844,11 +1853,14 @@ var LibraryJSEvents = { } } #else +#else // !OFFSCREENCANVAS_SUPPORT +#if !OFFSCREEN_FRAMEBUFFER if (contextAttributes['explicitSwapControl']) { - console.error('emscripten_webgl_create_context failed: explicitSwapControl is not supported, please rebuild with -s OFFSCREENCANVAS_SUPPORT=1 to enable targeting the experimental OffscreenCanvas specification!'); + console.error('emscripten_webgl_create_context failed: explicitSwapControl is not supported, please rebuild with -s OFFSCREENCANVAS_SUPPORT=1 to enable targeting the experimental OffscreenCanvas specification, or rebuild with -s OFFSCREEN_FRAMEBUFFER=1 to emulate explicitSwapControl in the absence of OffscreenCanvas support!'); return 0; } -#endif +#endif // ~!OFFSCREEN_FRAMEBUFFER +#endif // ~!OFFSCREENCANVAS_SUPPORT var contextHandle = GL.createContext(canvas, contextAttributes); return contextHandle; @@ -1881,6 +1893,16 @@ var LibraryJSEvents = { #endif return {{{ cDefine('EMSCRIPTEN_RESULT_INVALID_TARGET') }}}; } + +#if OFFSCREEN_FRAMEBUFFER + if (GL.currentContext.defaultFbo) { + GL.blitOffscreenFramebuffer(GL.currentContext); +#if GL_DEBUG + if (GL.currentContext.GLctx.commit) console.error('emscripten_webgl_commit_frame(): Offscreen framebuffer should never have gotten created when canvas is in OffscreenCanvas mode, since it is redundant and not necessary'); +#endif + return {{{ cDefine('EMSCRIPTEN_RESULT_SUCCESS') }}}; + } +#endif if (!GL.currentContext.GLctx.commit) { #if GL_DEBUG console.error('emscripten_webgl_commit_frame() failed: OffscreenCanvas is not supported by the current GL context!'); diff --git a/src/library_sdl.js b/src/library_sdl.js index 8b5e1b1393ef5..af909cb6c47dc 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -394,6 +394,11 @@ var LibrarySDL = { alpha: (SDL.glAttributes[3 /*SDL_GL_ALPHA_SIZE*/] > 0) }; +#if OFFSCREEN_FRAMEBUFFER + // TODO: Make SDL explicitly aware of whether it is being proxied or not, and set these to true only when proxying is being performed. + webGLContextAttributes.renderViaOffscreenBackBuffer = true; + webGLContextAttributes.preserveDrawingBuffer = true; +#endif var ctx = Browser.createContext(canvas, is_SDL_OPENGL, usePageCanvas, webGLContextAttributes); SDL.surfaces[surf] = { diff --git a/src/settings.js b/src/settings.js index ab29599c3bea0..4e311f4409d75 100644 --- a/src/settings.js +++ b/src/settings.js @@ -832,6 +832,7 @@ var TEXTDECODER = 1; // Is enabled, use the JavaScript TextDecoder API for strin var OFFSCREENCANVAS_SUPPORT = 0; // If set to 1, enables support for transferring canvases to pthreads and creating WebGL contexts in them, // as well as explicit swap control for GL contexts. This needs browser support for the OffscreenCanvas // specification. +var OFFSCREEN_FRAMEBUFFER = 0; // If set to 1, enables rendering to an offscreen render target first, and then finally flipping on to the screen. var FETCH_DEBUG = 0; // If nonzero, prints out debugging information in library_fetch.js diff --git a/src/struct_info.json b/src/struct_info.json index d4c8d42c65b39..eab261b0ab216 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -1386,7 +1386,8 @@ "majorVersion", "minorVersion", "enableExtensionsByDefault", - "explicitSwapControl" + "explicitSwapControl", + "renderViaOffscreenBackBuffer" ], "EmscriptenFullscreenStrategy": [ "scaleMode", diff --git a/system/include/emscripten/html5.h b/system/include/emscripten/html5.h index faccfd41234de..559e3c9291562 100644 --- a/system/include/emscripten/html5.h +++ b/system/include/emscripten/html5.h @@ -414,6 +414,7 @@ typedef struct EmscriptenWebGLContextAttributes { EM_BOOL enableExtensionsByDefault; EM_BOOL explicitSwapControl; + EM_BOOL renderViaOffscreenBackBuffer; } EmscriptenWebGLContextAttributes; extern void emscripten_webgl_init_context_attributes(EmscriptenWebGLContextAttributes *attributes);