[Fix] (Backend): apply framebuffer no-attachment defaults on both backends

This commit is contained in:
2026-10-12 08:32:51 -04:00
parent 3b0e6270b7
commit 8fff9363f7
6 changed files with 82 additions and 2 deletions
@@ -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<Int, 5> defaults = {
static_cast<Int>(record.Width), static_cast<Int>(record.Height),
static_cast<Int>(record.Layers),
std::min<Int>(static_cast<Int>(record.Samples), g_GLESCapabilities.MaxFramebufferSamples),
static_cast<Int>(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
@@ -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<Int, 5> 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.
@@ -842,6 +842,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> 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;
@@ -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<Uint32>(fbo.Width, limits.maxFramebufferWidth);
extent.height = std::min<Uint32>(fbo.Height, limits.maxFramebufferHeight);
layers = std::clamp<Uint32>(fbo.Layers, 1u, std::max<Uint32>(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<VkSampleCountFlagBits>(bit);
if (bit >= fbo.Samples) break;
}
}
passKey.noAttachmentSamples = static_cast<Uint32>(samples);
passHash = XXH64(&samples, sizeof(samples), passHash);
}
passKey.width = extent.width;
passKey.height = extent.height;
@@ -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) &&
@@ -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