From c741a938bc2ae9f1b60b94de29919c9bfb7f39a0 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sun, 2 Aug 2026 04:34:25 -0400 Subject: [PATCH] [Feat] (DirectGLES): emulate a depth/stencil blit into a multisample framebuffer Desktop GL replicates the source sample into every destination sample when the read framebuffer is single-sampled and the draw framebuffer is not. ES forbids the call outright - "an INVALID_OPERATION error is generated if SAMPLE_BUFFERS for the draw framebuffer is greater than zero" - so the blit did nothing at all, and every one of KHR-GL3x.packed_depth_stencil.blit's replicate iterations verified a destination that still held its clear values. Emulate it by drawing a full-screen triangle into the multisample framebuffer: every pixel is fully covered, so every sample of it receives the same value, which is precisely the replicate rule. The source rectangle is first copied into a scratch texture of its own format (both sides single-sampled, which ES does allow), then depth is written through gl_FragDepth and stencil - which has no shader output on ES - one bit plane at a time with REPLACE and a discard for the pixels whose source bit is clear. The draw runs inside the caller's framebuffer, so every piece of pipeline state it touches is read back and restored, including the per-draw-buffer colour masks the non-indexed glColorMask does not cover: the sync layer's shadow of the driver state has to stay true across this. Colour replicate is not emulated (it would need a sampler variant per component type); it now says so instead of failing silently. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 465 +++++++++++++++++- 1 file changed, 461 insertions(+), 4 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 6c517949..fe35c6f4 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2382,15 +2382,462 @@ namespace MobileGL::MG_Backend::DirectGLES { return resolved; } + // --------------------------------------------------------------------------------- + // Single-sample -> multisample blit ("replicate") + // + // Desktop GL replicates the source sample into every destination sample when the read + // framebuffer is single-sampled and the draw framebuffer is not. ES forbids the whole + // call ("INVALID_OPERATION if SAMPLE_BUFFERS for the draw framebuffer is greater than + // zero"), so the blit silently did nothing - KHR-GL3x.packed_depth_stencil.blit's + // second loop then read a destination that still held its clear values. + // + // Emulated by drawing a full-screen triangle into the multisample framebuffer: every + // pixel is fully covered, so every sample of it receives the same value, which is + // exactly what the replicate rule asks for. Depth comes from gl_FragDepth; stencil has + // no shader output on ES, so it is written one bit plane at a time with REPLACE and a + // discard for the pixels whose source bit is clear. + namespace ReplicateBlitImpl { + static Uint s_contextGeneration = ~0u; + static GLuint s_framebuffer = 0; + static GLuint s_texture = 0; + static GLenum s_textureFormat = 0; + static GLsizei s_textureWidth = 0; + static GLsizei s_textureHeight = 0; + static GLuint s_vertexArray = 0; + static GLuint s_depthProgram = 0; + static GLuint s_stencilProgram = 0; + static GLint s_depthUvTransform = -1; + static GLint s_stencilUvTransform = -1; + static GLint s_stencilBit = -1; + static Bool s_programsFailed = false; + + static const char* const kVertexSource = + "#version 300 es\n" + "uniform vec4 uUvTransform;\n" + "out vec2 vUv;\n" + "void main() {\n" + " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" + " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" + " vUv = p * uUvTransform.xy + uUvTransform.zw;\n" + "}\n"; + + static const char* const kDepthFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp sampler2D;\n" + "uniform sampler2D uSource;\n" + "in vec2 vUv;\n" + "void main() {\n" + " gl_FragDepth = texture(uSource, vUv).r;\n" + "}\n"; + + static const char* const kStencilFragmentSource = + "#version 300 es\n" + "precision highp float;\n" + "precision highp usampler2D;\n" + "uniform usampler2D uSource;\n" + "uniform uint uBit;\n" + "in vec2 vUv;\n" + "void main() {\n" + " if ((texture(uSource, vUv).r & uBit) == 0u) discard;\n" + "}\n"; + + static GLuint BuildProgram(const char* fragmentSource) { + const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER); + const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER); + if (vertexShader == 0 || fragmentShader == 0) { + return 0; + } + g_GLESFuncs.glShaderSource(vertexShader, 1, &kVertexSource, nullptr); + g_GLESFuncs.glCompileShader(vertexShader); + g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + g_GLESFuncs.glCompileShader(fragmentShader); + + const GLuint program = g_GLESFuncs.glCreateProgram(); + GLint linked = GL_FALSE; + if (program != 0) { + g_GLESFuncs.glAttachShader(program, vertexShader); + g_GLESFuncs.glAttachShader(program, fragmentShader); + g_GLESFuncs.glLinkProgram(program); + g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked); + } + g_GLESFuncs.glDeleteShader(vertexShader); + g_GLESFuncs.glDeleteShader(fragmentShader); + if (linked != GL_TRUE) { + if (program != 0) g_GLESFuncs.glDeleteProgram(program); + return 0; + } + return program; + } + + static Bool EnsureResources() { + if (s_contextGeneration != TextureImpl::g_textureContextGeneration) { + // The ids belonged to a dead context; the context reclaimed them with it. + s_framebuffer = 0; + s_texture = 0; + s_textureFormat = 0; + s_textureWidth = 0; + s_textureHeight = 0; + s_vertexArray = 0; + s_depthProgram = 0; + s_stencilProgram = 0; + s_programsFailed = false; + s_contextGeneration = TextureImpl::g_textureContextGeneration; + } + if (s_programsFailed) { + return false; + } + if (s_depthProgram == 0) { + s_depthProgram = BuildProgram(kDepthFragmentSource); + s_stencilProgram = BuildProgram(kStencilFragmentSource); + if (s_depthProgram == 0 || s_stencilProgram == 0) { + s_programsFailed = true; + MGLOG_E("BlitFramebuffer: could not build the multisample replicate programs"); + return false; + } + s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); + s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); + s_stencilBit = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uBit"); + } + if (s_framebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &s_framebuffer); + if (s_framebuffer == 0) return false; + } + if (s_vertexArray == 0) { + g_GLESFuncs.glGenVertexArrays(1, &s_vertexArray); + if (s_vertexArray == 0) return false; + } + return true; + } + + // Sized internal format of the read framebuffer's depth (or, failing that, stencil) + // attachment. The scratch copy has to use the very same one: ES rejects a + // depth/stencil blit between differing formats even when both sides are single-sampled. + static GLenum QueryReadDepthStencilFormat(GLenum* outAttachment) { + const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT}; + for (const GLenum attachment : attachments) { + GLint objectType = 0; + GLint objectName = 0; + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType); + g_GLESFuncs.glGetFramebufferAttachmentParameteriv(GL_READ_FRAMEBUFFER, attachment, + GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName); + if (objectName == 0) { + continue; + } + GLint internalFormat = 0; + if (objectType == GL_RENDERBUFFER) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(objectName)); + g_GLESFuncs.glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast(previous)); + } else if (objectType == GL_TEXTURE) { + GLint previous = 0; + g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(objectName)); + g_GLESFuncs.glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, + &internalFormat); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previous)); + } + if (internalFormat != 0) { + if (outAttachment) *outAttachment = attachment; + return static_cast(internalFormat); + } + } + return 0; + } + + static Bool FormatHasDepth(GLenum internalFormat) { + return internalFormat == GL_DEPTH_COMPONENT16 || internalFormat == GL_DEPTH_COMPONENT24 || + internalFormat == GL_DEPTH_COMPONENT32F || internalFormat == GL_DEPTH24_STENCIL8 || + internalFormat == GL_DEPTH32F_STENCIL8; + } + + static Bool FormatHasStencil(GLenum internalFormat) { + return internalFormat == GL_DEPTH24_STENCIL8 || internalFormat == GL_DEPTH32F_STENCIL8 || + internalFormat == GL_STENCIL_INDEX8; + } + + static GLenum ScratchAttachmentFor(GLenum internalFormat) { + if (FormatHasDepth(internalFormat) && FormatHasStencil(internalFormat)) { + return GL_DEPTH_STENCIL_ATTACHMENT; + } + return FormatHasDepth(internalFormat) ? GL_DEPTH_ATTACHMENT : GL_STENCIL_ATTACHMENT; + } + } // namespace ReplicateBlitImpl + + // Returns true when the request was serviced (or is not this fallback's business). + static Bool ReplicateBlitIntoMultisampleDraw(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, + GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) { + using namespace ReplicateBlitImpl; + if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) == 0) { + return false; + } + if (!EnsureResources()) { + return false; + } + + const GLsizei srcWidth = static_cast(std::abs(srcX1 - srcX0)); + const GLsizei srcHeight = static_cast(std::abs(srcY1 - srcY0)); + const GLsizei dstWidth = static_cast(std::abs(dstX1 - dstX0)); + const GLsizei dstHeight = static_cast(std::abs(dstY1 - dstY0)); + if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { + return false; + } + + GLenum readAttachment = GL_DEPTH_ATTACHMENT; + const GLenum sourceFormat = QueryReadDepthStencilFormat(&readAttachment); + if (sourceFormat == 0) { + return false; + } + const Bool wantDepth = (mask & GL_DEPTH_BUFFER_BIT) != 0 && FormatHasDepth(sourceFormat); + const Bool wantStencil = (mask & GL_STENCIL_BUFFER_BIT) != 0 && FormatHasStencil(sourceFormat); + if (!wantDepth && !wantStencil) { + return false; + } + // Reading the stencil back needs a stencil texture view, which is ES 3.1 state. + if (wantStencil && !g_GLESFuncs.glTexParameteri) { + return false; + } + + GLint previousDraw = 0; + GLint previousRead = 0; + g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDraw); + g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previousRead); + GLint previousActiveTexture = 0; + g_GLESFuncs.glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture); + + // Copy the source rectangle into a scratch texture of its own format: both sides of + // that blit are single-sampled, which ES does allow. + Bool ok = true; + if (s_texture == 0 || s_textureFormat != sourceFormat || s_textureWidth < srcWidth || + s_textureHeight < srcHeight) { + if (s_texture != 0) { + g_GLESFuncs.glDeleteTextures(1, &s_texture); // immutable storage cannot be resized + s_texture = 0; + } + s_textureWidth = std::max(s_textureWidth, srcWidth); + s_textureHeight = std::max(s_textureHeight, srcHeight); + g_GLESFuncs.glGenTextures(1, &s_texture); + if (s_texture == 0) { + ok = false; + } else { + g_GLESFuncs.glActiveTexture(GL_TEXTURE0); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); + DrainBlitErrors(); + g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, sourceFormat, s_textureWidth, s_textureHeight); + ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + s_textureFormat = ok ? sourceFormat : 0; + } + } + + if (ok) { + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_framebuffer); + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, ScratchAttachmentFor(sourceFormat), GL_TEXTURE_2D, + s_texture, 0); + ok = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE; + } + if (ok) { + const GLint left = std::min(srcX0, srcX1); + const GLint bottom = std::min(srcY0, srcY1); + DrainBlitErrors(); + g_GLESFuncs.glBlitFramebuffer(left, bottom, left + srcWidth, bottom + srcHeight, 0, 0, srcWidth, srcHeight, + mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT), GL_NEAREST); + ok = g_GLESFuncs.glGetError() == GL_NO_ERROR; + } + + g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); + g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(previousRead)); + FramebufferImpl::InvalidateFramebufferBindingCache(); + if (!ok) { + g_GLESFuncs.glActiveTexture(static_cast(previousActiveTexture)); + MGLOG_E("BlitFramebuffer: could not stage the source for the multisample replicate"); + return false; + } + + // Everything below draws into the caller's multisample draw framebuffer, so the + // pipeline state it depends on is saved and put back byte for byte - the sync layer's + // shadow of the driver state has to stay true. + GLint previousProgram = 0; + GLint previousVertexArray = 0; + GLint previousTexture = 0; + GLint previousViewport[4] = {0, 0, 0, 0}; + GLint previousScissorBox[4] = {0, 0, 0, 0}; + GLboolean previousColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE}; + GLint previousDepthFunc = GL_LESS; + GLboolean previousDepthMask = GL_TRUE; + GLint previousStencilFunc[2] = {GL_ALWAYS, GL_ALWAYS}; + GLint previousStencilRef[2] = {0, 0}; + GLint previousStencilValueMask[2] = {~0, ~0}; + GLint previousStencilWriteMask[2] = {~0, ~0}; + GLint previousStencilFail[2] = {GL_KEEP, GL_KEEP}; + GLint previousStencilDepthFail[2] = {GL_KEEP, GL_KEEP}; + GLint previousStencilPass[2] = {GL_KEEP, GL_KEEP}; + g_GLESFuncs.glGetIntegerv(GL_CURRENT_PROGRAM, &previousProgram); + g_GLESFuncs.glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &previousVertexArray); + g_GLESFuncs.glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTexture); + g_GLESFuncs.glGetIntegerv(GL_VIEWPORT, previousViewport); + g_GLESFuncs.glGetIntegerv(GL_SCISSOR_BOX, previousScissorBox); + g_GLESFuncs.glGetBooleanv(GL_COLOR_WRITEMASK, previousColorMask); + g_GLESFuncs.glGetIntegerv(GL_DEPTH_FUNC, &previousDepthFunc); + g_GLESFuncs.glGetBooleanv(GL_DEPTH_WRITEMASK, &previousDepthMask); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FUNC, &previousStencilFunc[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FUNC, &previousStencilFunc[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_REF, &previousStencilRef[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_REF, &previousStencilRef[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_VALUE_MASK, &previousStencilValueMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &previousStencilValueMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_WRITEMASK, &previousStencilWriteMask[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &previousStencilWriteMask[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_FAIL, &previousStencilFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_FAIL, &previousStencilFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, &previousStencilDepthFail[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, &previousStencilDepthFail[1]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &previousStencilPass[0]); + g_GLESFuncs.glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &previousStencilPass[1]); + + struct CapabilityState { + GLenum cap; + GLboolean enabled; + }; + CapabilityState capabilities[] = { + {GL_SCISSOR_TEST, GL_FALSE}, {GL_DEPTH_TEST, GL_FALSE}, + {GL_STENCIL_TEST, GL_FALSE}, {GL_CULL_FACE, GL_FALSE}, + {GL_BLEND, GL_FALSE}, {GL_RASTERIZER_DISCARD, GL_FALSE}, + {GL_POLYGON_OFFSET_FILL, GL_FALSE}, {GL_SAMPLE_ALPHA_TO_COVERAGE, GL_FALSE}, + {GL_SAMPLE_COVERAGE, GL_FALSE}, {GL_SAMPLE_MASK, GL_FALSE}, + }; + for (CapabilityState& capability : capabilities) { + capability.enabled = g_GLESFuncs.glIsEnabled(capability.cap); + } + + const GLint dstLeft = std::min(dstX0, dstX1); + const GLint dstBottom = std::min(dstY0, dstY1); + const Bool mirrorX = (srcX1 > srcX0) != (dstX1 > dstX0); + const Bool mirrorY = (srcY1 > srcY0) != (dstY1 > dstY0); + // The scratch holds the source rectangle at its origin, so the texture is larger than + // the copied region: scale the [0,1] quad coordinates down to the region it occupies. + const Float uvScaleX = static_cast(srcWidth) / static_cast(s_textureWidth); + const Float uvScaleY = static_cast(srcHeight) / static_cast(s_textureHeight); + const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY, + mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f}; + + g_GLESFuncs.glBindVertexArray(s_vertexArray); + g_GLESFuncs.glActiveTexture(GL_TEXTURE0); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); + g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight); + g_GLESFuncs.glScissor(dstLeft, dstBottom, dstWidth, dstHeight); + g_GLESFuncs.glEnable(GL_SCISSOR_TEST); + g_GLESFuncs.glDisable(GL_CULL_FACE); + g_GLESFuncs.glDisable(GL_BLEND); + g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD); + g_GLESFuncs.glDisable(GL_POLYGON_OFFSET_FILL); + g_GLESFuncs.glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE); + g_GLESFuncs.glDisable(GL_SAMPLE_COVERAGE); + g_GLESFuncs.glDisable(GL_SAMPLE_MASK); + g_GLESFuncs.glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + DrainBlitErrors(); + + if (wantDepth) { + if (FormatHasStencil(sourceFormat)) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + } + g_GLESFuncs.glUseProgram(s_depthProgram); + g_GLESFuncs.glUniform4f(s_depthUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], + uvTransform[3]); + g_GLESFuncs.glEnable(GL_DEPTH_TEST); + g_GLESFuncs.glDepthFunc(GL_ALWAYS); + g_GLESFuncs.glDepthMask(GL_TRUE); + g_GLESFuncs.glDisable(GL_STENCIL_TEST); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + } + + if (wantStencil) { + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + g_GLESFuncs.glUseProgram(s_stencilProgram); + g_GLESFuncs.glUniform4f(s_stencilUvTransform, uvTransform[0], uvTransform[1], uvTransform[2], + uvTransform[3]); + g_GLESFuncs.glDisable(GL_DEPTH_TEST); + g_GLESFuncs.glDepthMask(GL_FALSE); + g_GLESFuncs.glEnable(GL_STENCIL_TEST); + // The bit planes are written by ORing in the set bits, so the destination has to + // start from zero. The blit overwrites the whole rectangle anyway, and the scissor + // keeps the clear inside it. + const GLint zero = 0; + g_GLESFuncs.glStencilMask(0xFFu); + g_GLESFuncs.glClearBufferiv(GL_STENCIL, 0, &zero); + g_GLESFuncs.glStencilFunc(GL_ALWAYS, 0xFF, 0xFFu); + g_GLESFuncs.glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + for (Uint bit = 0; bit < 8; ++bit) { + g_GLESFuncs.glStencilMask(1u << bit); + g_GLESFuncs.glUniform1ui(s_stencilBit, 1u << bit); + g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3); + } + g_GLESFuncs.glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + } + + const Bool replicated = g_GLESFuncs.glGetError() == GL_NO_ERROR; + + g_GLESFuncs.glUseProgram(static_cast(previousProgram)); + g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previousTexture)); + g_GLESFuncs.glActiveTexture(static_cast(previousActiveTexture)); + g_GLESFuncs.glBindVertexArray(static_cast(previousVertexArray)); + g_GLESFuncs.glViewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]); + g_GLESFuncs.glScissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], + previousScissorBox[3]); + g_GLESFuncs.glColorMask(previousColorMask[0], previousColorMask[1], previousColorMask[2], + previousColorMask[3]); + g_GLESFuncs.glDepthFunc(static_cast(previousDepthFunc)); + g_GLESFuncs.glDepthMask(previousDepthMask); + const GLenum faces[2] = {GL_FRONT, GL_BACK}; + for (SizeT face = 0; face < 2; ++face) { + g_GLESFuncs.glStencilFuncSeparate(faces[face], static_cast(previousStencilFunc[face]), + previousStencilRef[face], + static_cast(previousStencilValueMask[face])); + g_GLESFuncs.glStencilOpSeparate(faces[face], static_cast(previousStencilFail[face]), + static_cast(previousStencilDepthFail[face]), + static_cast(previousStencilPass[face])); + g_GLESFuncs.glStencilMaskSeparate(faces[face], static_cast(previousStencilWriteMask[face])); + } + for (const CapabilityState& capability : capabilities) { + if (capability.enabled) { + g_GLESFuncs.glEnable(capability.cap); + } else { + g_GLESFuncs.glDisable(capability.cap); + } + } + // The per-draw-buffer colour masks are not covered by the non-indexed glColorMask above. + for (Uint index = 0; index < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++index) { + const BoolVec4& colorMask = RenderStateImpl::g_syncedRenderStateParameters.ColorMasks[index]; + if (g_GLESFuncs.glColorMaski) { + g_GLESFuncs.glColorMaski(index, colorMask.x() ? GL_TRUE : GL_FALSE, colorMask.y() ? GL_TRUE : GL_FALSE, + colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE); + } + } + DrainBlitErrors(); + + if (!replicated) { + MGLOG_E("BlitFramebuffer: multisample replicate fallback failed"); + } + return replicated; + } + static void IssueBlitWithResolveFallback(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { DrainBlitErrors(); g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); - if (g_GLESFuncs.glGetError() == GL_NO_ERROR || (mask & GL_COLOR_BUFFER_BIT) == 0) { + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { return; } - // Only the multisample-resolve format mismatch is worth a second attempt; a - // source that is not multisampled would have hit the same restriction on desktop. + // Two ES restrictions desktop GL does not have are worth a second attempt: a + // multisample resolve that also converts colour format, and any blit into a + // multisample draw framebuffer. Both need to know how the two sides are sampled. GLint readSamples = 0; GLint drawSamples = 0; { @@ -2406,7 +2853,17 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(previousDraw)); FramebufferImpl::InvalidateFramebufferBindingCache(); } - if (readSamples <= 0 || drawSamples > 0) { + if (readSamples <= 0 && drawSamples > 0) { + // Single-sample source into a multisample destination: ES rejects the call + // outright, desktop GL replicates the source sample into every destination one. + if (ReplicateBlitIntoMultisampleDraw(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask)) { + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + MGLOG_E("BlitFramebuffer: colour replicate into a multisample draw framebuffer is not emulated"); + } + } + return; + } + if (readSamples <= 0 || drawSamples > 0 || (mask & GL_COLOR_BUFFER_BIT) == 0) { return; } if (ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter) &&