From 8fff9363f733d19bff1c3983d55d78dd0598eb6c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:04:23 -0400 Subject: [PATCH] [Fix] (Backend): apply framebuffer no-attachment defaults on both backends --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 34 +++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 5 +++ .../DirectVulkan/Renderer/VulkanRenderer.h | 3 ++ .../DirectVulkan/Renderer/WireDraw.inc | 32 +++++++++++++++-- .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 6 ++++ .../MG_Util/BackendLoaders/OpenGL/Loader.h | 4 +++ 6 files changed, 82 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 04ce7b76..35abb59f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -9686,6 +9686,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // force. 0 is never a live hash, so a zeroed memo is a guaranteed miss. m_syncedRecordHashes = {0}; m_syncedAttachmentRecordHash = 0; + m_syncedNoAttachmentDefaultsValid = false; } static Bool IsSnormFormat(TextureInternalFormat format) { @@ -10465,6 +10466,39 @@ namespace MobileGL::MG_Backend::DirectGLES { NoteFramebufferTextureAttachments(fbo, record); } + // 4. ARB_framebuffer_no_attachments. With no attachment at all the record's geometry + // IS the framebuffer's default width/height/layers/samples (FillGeometry), and the + // driver FBO needs them too: without a default size an attachment-less ES + // framebuffer is incomplete and every draw into it is dropped. With attachments the + // defaults are ignored by GL, so they are only pushed for the empty shape. + Bool hasAnyAttachment = !(record.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Depth.Res)) || + !(record.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Stencil.Res)); + for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && !hasAnyAttachment; ++i) { + hasAnyAttachment = !(record.Color[i].Kind == MG_Pipe::kMGPipeSurfaceKindNone || + MG_Pipe::MGPipeHandleIsNull(record.Color[i].Res)); + } + if (!hasAnyAttachment) { + const Array defaults = { + static_cast(record.Width), static_cast(record.Height), + static_cast(record.Layers), + std::min(static_cast(record.Samples), g_GLESCapabilities.MaxFramebufferSamples), + static_cast(record.FixedSampleLocations != 0 ? GL_TRUE : GL_FALSE)}; + if (!m_syncedNoAttachmentDefaultsValid || m_syncedNoAttachmentDefaults != defaults) { + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_WIDTH, defaults[0]); + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_HEIGHT, defaults[1]); + if (g_GLESCapabilities.SupportsFramebufferDefaultLayers) { + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_LAYERS, defaults[2]); + } + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_SAMPLES, defaults[3]); + g_GLESFuncs.glFramebufferParameteri(glFBOTarget, GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS, + defaults[4]); + m_syncedNoAttachmentDefaults = defaults; + m_syncedNoAttachmentDefaultsValid = true; + } + } + // The walk itself can re-mint a driver id (the storage sync behind a texture // surface), invalidating points this walk already attached. Re-enter until the // generation is quiescent, exactly as the object form does - every pass syncs each diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 7dc54cba..c80815fb 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -2258,6 +2258,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // INVALID_OPERATION. Keying on the object is what makes "this hash is applied" mean // "these points are on the driver now". Uint64 m_syncedAttachmentRecordHash = 0; + // The ARB_framebuffer_no_attachments defaults (width, height, layers, samples, + // fixed sample locations) last pushed to the driver FBO with glFramebufferParameteri. + // Like the attachments they belong to the driver OBJECT, so one memo, not per target. + Array m_syncedNoAttachmentDefaults = {0, 0, 0, 0, 0}; + Bool m_syncedNoAttachmentDefaultsValid = false; // P5e (fb): the read-buffer decision, taken from the record alone. Both // SyncReadBufferToBackend overloads funnel through it, so the rule lives in one // place and the object form is visibly the half that only finds the handle. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index a98dc602..3c025087 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -842,6 +842,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector colorReferences; Uint32 depthReference = VK_ATTACHMENT_UNUSED; Uint32 width = 0, height = 0, layers = 1; + // The rasterization sample count of a pass with no attachments (0 otherwise), where + // nothing else in the key would tell two such passes apart. + Uint32 noAttachmentSamples = 0; Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0; VkSwapchainKHR swapchain = VK_NULL_HANDLE; Uint32 swapchainImageIndex = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index bfa66490..b96e5df9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -380,8 +380,36 @@ else if (fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) depth.attachment = add(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); if (attachments.empty()) { - MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); - return false; + // ARB_framebuffer_no_attachments (GL 4.6 core 9.2): a framebuffer with no image + // attached rasterizes at its default width/height/layers/samples, which the record + // carries in that case (FillGeometry). Vulkan expresses it directly as a render pass + // with no attachments over a framebuffer of that extent. + const Bool hasAnyAttachment = fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone || + fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone || + std::any_of(std::begin(fbo.Color), std::end(fbo.Color), [](const MG_Pipe::MGPSurface& surface) { + return surface.Kind != MG_Pipe::kMGPipeSurfaceKindNone; + }); + if (fbo.IsDefault || hasAnyAttachment || fbo.Width == 0 || fbo.Height == 0) { + MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); + return false; + } + const auto& limits = m_physicalDevice.properties.limits; + extent.width = std::min(fbo.Width, limits.maxFramebufferWidth); + extent.height = std::min(fbo.Height, limits.maxFramebufferHeight); + layers = std::clamp(fbo.Layers, 1u, std::max(limits.maxFramebufferLayers, 1u)); + // The smallest count the device rasterizes attachment-less passes at that covers the + // request (GL lets the effective count exceed the requested one), else its largest. + samples = VK_SAMPLE_COUNT_1_BIT; + const VkSampleCountFlags supported = limits.framebufferNoAttachmentsSampleCounts; + if (fbo.Samples > 1) { + for (Uint32 bit = VK_SAMPLE_COUNT_2_BIT; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) { + if ((supported & bit) == 0) continue; + samples = static_cast(bit); + if (bit >= fbo.Samples) break; + } + } + passKey.noAttachmentSamples = static_cast(samples); + passHash = XXH64(&samples, sizeof(samples), passHash); } passKey.width = extent.width; passKey.height = extent.height; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 38390b34..40b04a98 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1128,6 +1128,7 @@ namespace MobileGL::MG_Util::BackendLoader { Bool hasTessellationShaderExtension = false; Bool hasClipControlExtension = false; Bool hasPolygonOffsetClampExtension = false; + Bool hasGeometryShaderExtension = false; for (GLint i = 0; i < extCount; ++i) { const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i); if (extension) { @@ -1194,6 +1195,10 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_KHR_parallel_shader_compile") == 0) { caps.SupportsParallelShaderCompile = true; } + if (std::strcmp(extension, "GL_EXT_geometry_shader") == 0 || + std::strcmp(extension, "GL_OES_geometry_shader") == 0) { + hasGeometryShaderExtension = true; + } if (std::strcmp(extension, "GL_EXT_polygon_offset_clamp") == 0) { hasPolygonOffsetClampExtension = true; } @@ -1293,6 +1298,7 @@ namespace MobileGL::MG_Util::BackendLoader { (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2); const Bool esAtLeast31 = caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1); + caps.SupportsFramebufferDefaultLayers = esAtLeast32 || hasGeometryShaderExtension; caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr && glesFuncs.glDrawElementsIndirect != nullptr; caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) && diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 2a955941..720c6dc5 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1223,6 +1223,10 @@ namespace MobileGL { // it the GL_POLYGON_OFFSET_CLAMP value cannot be applied and only glPolygonOffset's // unclamped offset reaches the driver. Bool SupportsPolygonOffsetClamp = false; + // GL_FRAMEBUFFER_DEFAULT_LAYERS is a legal glFramebufferParameteri pname: ES 3.2 core, + // or GL_EXT/OES_geometry_shader before it. The other four no-attachment defaults are + // ES 3.1 core and need no flag. + Bool SupportsFramebufferDefaultLayers = false; // GL_NV_shader_noperspective_interpolation is present: the driver accepts the // `noperspective` interpolation qualifier in ESSL. GLES core has none, so without this // SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back