[Refactor] (P13 W6e): the backends' frontend-framebuffer clear, blit and readback entries go - the verb port owns those slots in every mode - with Magma's frontend render-pass, pending-clear and renderbuffer halves.

This commit is contained in:
2026-10-06 18:57:15 -04:00
parent ec7fc40ba5
commit dab827d54f
23 changed files with 24 additions and 7056 deletions
@@ -1605,7 +1605,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
@@ -1633,18 +1632,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
funcsTable.GL.WaitSync = WaitSync;
File diff suppressed because it is too large Load Diff
@@ -46,8 +46,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
@@ -71,21 +69,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
@@ -98,7 +83,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GenerateMipmap(GLenum target);
const GLubyte* GetString(GLenum name);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
-579
View File
@@ -10389,91 +10389,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttachmentRecordHash = 0;
}
static Bool SyncAttachmentObject(GLenum glFBOTarget,
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject,
GLenum glBackendAttachment) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
SharedPtr<TextureImpl::BackendTextureObject> backendTextureObject;
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
if (auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get())) {
backendTextureObject = *backendTextureSlot;
} else {
auto& newTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject);
if (!newTextureSlot) {
newTextureSlot = MakeShared<TextureImpl::BackendTextureObject>();
}
backendTextureObject = newTextureSlot;
}
if (!backendTextureObject) {
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
return false;
}
backendTextureObject->SyncMipmapsToBackend(textureObject);
// From here on every draw, clear and blit into this framebuffer can write the
// texture behind the shadow's back.
TextureImpl::NoteDriverSideTextureWrite(textureObject);
if (attachmentObject.IsLayered()) {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
} else if (const auto uploadTarget = attachmentObject.GetTextureUploadTarget();
uploadTarget == TextureUploadTarget::Texture3D ||
uploadTarget == TextureUploadTarget::Texture2DArray ||
uploadTarget == TextureUploadTarget::Texture1DArray ||
uploadTarget == TextureUploadTarget::CubeMapArray ||
uploadTarget == TextureUploadTarget::Texture2DMultisampleArray) {
// Single slice/layer of a 3D or array texture: ES has no
// glFramebufferTexture3D, layers attach via glFramebufferTextureLayer.
g_GLESFuncs.glFramebufferTextureLayer(glFBOTarget, glBackendAttachment,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()),
static_cast<GLint>(attachmentObject.GetTextureLayer()));
} else {
auto glTextureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
attachmentObject.GetTextureUploadTarget());
if (glTextureTarget == GL_UNKNOWN_MGL) {
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
}
// glBindTexture rejects cube-face enums (INVALID_ENUM with no
// bind, while Bind() would still record the cube-map cache slot
// as bound): bind via the owning cube target; the attach below
// keeps the face target.
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
}
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on
// a first attach), named debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* backendRenderbufferSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *backendRenderbufferSlot;
} else {
auto& newRenderbufferSlot =
RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
if (!newRenderbufferSlot) {
newRenderbufferSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
}
backendRenderbufferObject = newRenderbufferSlot;
}
backendRenderbufferObject->SyncToBackend(renderbufferObject);
backendRenderbufferObject->Bind();
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
backendRenderbufferObject->GetBackendRenderbufferId());
}
return true;
}
static Bool IsSnormFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
@@ -10502,53 +10417,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
static Bool IsSnormFallbackAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsSnormFormat(textureObject->GetFormat()) &&
TextureImpl::ShouldUseCaveatTextureFormat(textureObject->GetFormat(), textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
IsSnormFormat(renderbufferObject->GetInternalFormat()) &&
TextureImpl::ShouldUseCaveatRenderbufferFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
static Bool IsUnormFallbackAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsUnormFormat(textureObject->GetFormat()) &&
TextureImpl::ShouldUseCaveatTextureFormat(textureObject->GetFormat(), textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
IsUnormFormat(renderbufferObject->GetInternalFormat()) &&
TextureImpl::ShouldUseCaveatRenderbufferFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
Bool IsAlphaWidenedColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(),
textureObject->GetTarget());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject &&
TextureImpl::BackendRenderbufferFormatAddsAlpha(renderbufferObject->GetInternalFormat());
}
return false;
}
Uint32 g_alphaWidenedDrawBufferMask = 0;
Uint32 g_integerColorDrawBufferMask = 0;
@@ -10585,19 +10453,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
static Bool IsIntegerColorAttachment(
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) {
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsIntegerColorFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsIntegerColorFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
// THE FOUR CROSS-OBJECT MASKS, ANSWERED FROM THE RECORD (ID-12 DV-5, review M-3 / DV-5).
//
// All four reduce to (internal format, TEXTURE TARGET) for a texture attachment and to
@@ -10850,23 +10705,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo) {
using FBO = MG_State::GLState::FramebufferObject;
const auto& drawBuffers = fbo.GetDrawBuffers();
Uint32 mask = 0;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = drawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
if (IsAlphaWidenedColorAttachment(fbo.GetAttachment(frontendBuf))) {
mask |= (1u << i);
}
}
return mask;
}
// The read attachment's storage carries an alpha channel its frontend format does not
// (the three-channel colour-renderable widening). GL answers such a read with 1.0, but
// the storage holds whatever the draw wrote there, so the readback has to overwrite it.
@@ -11011,49 +10849,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
ApplyReadBufferFromRecord(*record, 0);
}
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
return;
}
// P4a (D-C2): the READ buffer is answered from the RESOLVED read surface of the READ
// framebuffer's own record, which is what structurally closes the read-buffer
// shared-FBO defect class. The comment two lines below - "when this is reached from
// SyncCurrentFBO's 'same FBO as draw' skip path" - describes a hazard that becomes
// unrepresentable: the record says which target it is, and ReadSurface was resolved
// from the read framebuffer's own read buffer before it ever crossed.
//
// The record carries the resolved SURFACE and not an index, so the attachment POINT
// is recovered by matching it against Color[] - the one place both spellings sit
// side by side. An empty surface (Res == the null handle, which D-C1 makes the
// spelling of "no attachment" independently of Kind's numbering) is
// FramebufferAttachmentType::None, i.e. GL_NONE. A surface that matches no colour
// point cannot be a legal read buffer and is refused rather than guessed.
//
// P5e (fb): THAT DECISION now lives in ApplyReadBufferFromRecord, which takes no
// frontend object; what is left in this overload is the monolith glue that finds
// the handle by identity. Under an active transport nothing reaches it -
// SyncToBackendByHandle runs the record form directly.
// P5c (hd): with an active transport the handle is the caller's
// (m_pushedSyncHandle), never the client allocator's - see SyncToBackend.
const MG_Pipe::MGPipeHandle fbo =
m_pushedSyncHandle;
// ID-19: the OBJECT's record, not "the record of whatever is bound to READ". A
// framebuffer whose read buffer is being pushed need not be the read binding at
// all - glNamedFramebufferReadBuffer and the DSA clears reach here by name - and
// ReadSurface is resolved from THIS framebuffer's own read buffer under every
// Target, Named included (c0e's MGPFramebufferState comment).
const auto* record = PushedFramebufferRecord(fbo);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so "
"its read buffer cannot be pushed (handle {%u, %u})",
stateFBOObject->GetExternalIndex(), fbo.Slot, fbo.Gen);
return;
}
ApplyReadBufferFromRecord(*record, stateFBOObject->GetExternalIndex());
return;
}
Bool BackendFramebufferObject::RecomputeBackendColorSlots(
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers) {
// Only the first GL_MAX_COLOR_ATTACHMENTS points exist in the backend. The frontend's own
@@ -11143,380 +10938,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return moved;
}
void BackendFramebufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateFBOObject) {
MGLOG_E_ONCE("State FBO object is null, cannot sync to backend.");
return;
}
MGLOG_D("Syncing FBO with backend ID %u to backend for state ID %u, as %s FBO", m_backendFBOId,
stateFBOObject->GetExternalIndex(), (asTarget == FramebufferTarget::Draw ? "DRAW" : "READ"));
GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
// P4a (D-C2 as corrected by ID-19): THE RECORD OF THIS FRAMEBUFFER OBJECT, resolved
// BEFORE the driver framebuffer is bound.
//
// The order is the whole of C-1's fix. v1 bound first and refused afterwards, so a
// glClearNamedFramebufferfv / glBlitNamedFramebuffer on a framebuffer that is bound
// to neither target left this twin's freshly minted driver FBO bound with NO
// ATTACHMENTS and the caller then issued the clear or the blit against it -
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
// correctly. What the reorder buys is exactly this and no more: THE REFUSAL ITSELF
// HAS NO DRIVER EFFECT - it no longer mints and binds a driver FBO as a side effect
// of declining. It does NOT decide the driver's binding (review N-1): every caller
// binds immediately afterwards and none of them looks at a return value -
// SyncAndBindFramebufferObject is SyncToBackend then an unconditional Bind(target)
// (DirectGLES.cpp:3292-3293, the path all five DSA entry points take), and
// BindCurrentFBO binds the current FBO's twin whether or not SyncCurrentFBO synced
// it. So an unconfigured framebuffer can still end up bound; what the log line
// promises is only that this function did not put it there.
//
// The record is per FRAMEBUFFER OBJECT and is found by handle, so it describes this
// object whether it is bound to Draw, to Read, to both or to neither, and its own
// Target is NOT compared against `asTarget`: a Named record legitimately names no
// binding at all. What is still gated on `asTarget` is what reaches the DRIVER's
// bound target - glDrawBuffers and the four cross-object masks below, which stay
// Draw-only for the reason the OIT comment gives.
const MG_Pipe::MGPFramebufferState* pushedRecord = nullptr;
FramebufferObject::FramebufferAttachmentArray pushedDrawBuffers{};
// MONOLITH GLUE, named as such: the Framebuffer handle of an object this backend
// still arrives holding. A framebuffer has a handle but NO wire lifetime (D-I2) -
// there is no framebuffer create or destroy in the catalogue and none is invented
// - so the handle exists purely to key set_framebuffer_state, which is exactly
// what it is used for here.
//
// P5c (hd): with an active transport the handle is the one the caller is applying
// (m_pushedSyncHandle - the record-driven sync set it), and the client allocator
// is never probed (T2). A null there means a caller reached this arm without a
// record, which the null-record refusal below names.
const MG_Pipe::MGPipeHandle fbo =
m_pushedSyncHandle;
pushedRecord = PushedFramebufferRecord(fbo);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so it "
"is not configured here; nothing is bound by this refusal, but the "
"caller's own bind still targets it (handle {%u, %u})",
stateFBOObject->GetExternalIndex(), fbo.Slot, fbo.Gen);
return;
}
DecodePushedDrawBuffers(*pushedRecord, pushedDrawBuffers.data());
Bind(asTarget);
// -------------------- Connect attachments (set buffers) -----------------------
// 1. Remap draw buffers
const FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers =
pushedRecord != nullptr ? pushedDrawBuffers : stateFBOObject->GetDrawBuffers();
Bool drawBufferClean = false;
if (memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0) {
drawBufferClean = true;
}
// glDrawBuffers writes the state of the FBO bound to GL_DRAW_FRAMEBUFFER.
// When this object is only bound as the READ target the call would land on
// whatever framebuffer is draw-bound AND falsely stamp this object's memo,
// so the later draw-target sync skips as "clean" while the real state is
// stale (Minecraft 26.x OIT: the scratch clear-FBO kept draw buffers NONE
// from its blit-destination configuration, silently dropping every
// offscreen color clear).
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
int nEffectiveBuffers = 0;
for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
auto& frontendBuf = stateDrawBuffers[i];
if (frontendBuf == FramebufferAttachmentType::None) {
m_backendDrawBuffers[i] = GL_NONE;
continue;
}
// Create compacted mapping
if (frontendBuf == FramebufferAttachmentType::FrontLeft ||
frontendBuf == FramebufferAttachmentType::FrontRight ||
frontendBuf == FramebufferAttachmentType::BackLeft ||
frontendBuf == FramebufferAttachmentType::BackRight) {
MGLOG_D("%s: frontend buf token found for default fbo, shouldn't remap", __func__);
m_backendDrawBuffers[i] = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendBuf);
} else {
m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i;
}
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
// The line above pinned backend point s to draw-buffer slot s, so the images have to
// be moved under those points. Rebuild the whole colour map and, when anything moved,
// also drop the read-buffer memo: SyncReadBufferToBackend keys it on the frontend
// enum alone, which does not change when the point under it does.
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
}
MGLOG_D("DBAPPLY beFbo=%u target=%d n=%d db0=0x%x feDb0=%d", m_backendFBOId, (int)asTarget,
nEffectiveBuffers, m_backendDrawBuffers[0], (int)stateDrawBuffers[0]);
}
if (asTarget == FramebufferTarget::Draw) {
Uint32 snormClampOutputMask = 0;
Uint32 unormClampOutputMask = 0;
Uint32 alphaWidenedMask = 0;
Uint32 integerColorMask = 0;
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
// ID-12 DV-5 / M-3: on the handle arm the four masks are answered from the
// record's own surface for the point this draw buffer names, and the frontend
// attachment object below is never touched - the whole arm returns here.
// A draw buffer that names a point the record cannot describe is D-C3's
// refusal arriving too late to refuse, so it is loud and contributes no bit.
if (pushedRecord != nullptr) {
const MG_Pipe::MGPSurface* pushedSurface =
PushedSurfaceForAttachment(*pushedRecord, frontendBuf);
if (pushedSurface == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's draw buffer %u names colour point %d, "
"which its record does not describe (the record carries %u "
"colour points) - contributing no fallback mask bit",
stateFBOObject->GetExternalIndex(), i,
static_cast<Int>(frontendBuf) -
static_cast<Int>(FramebufferAttachmentType::Color0),
MG_Pipe::kMGPipeMaxColorAttachments);
continue;
}
if (IsSnormFallbackSurface(*pushedSurface)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackSurface(*pushedSurface)) {
unormClampOutputMask |= (1u << i);
}
if (IsAlphaWidenedColorSurface(*pushedSurface)) {
alphaWidenedMask |= (1u << i);
}
if (IsIntegerColorSurface(*pushedSurface)) {
integerColorMask |= (1u << i);
}
continue;
}
const auto& attachmentObject = stateFBOObject->GetAttachment(frontendBuf);
if (IsSnormFallbackAttachment(attachmentObject)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackAttachment(attachmentObject)) {
unormClampOutputMask |= (1u << i);
}
// Independent of the two above: a widened attachment can be SNORM
// (GL_RGB8_SNORM -> GL_RGBA16F, which also clamps) or not (GL_SRGB8 ->
// GL_SRGB8_ALPHA8, which does not), so it gets its own bit rather than an
// `else if` branch of theirs.
if (IsAlphaWidenedColorAttachment(attachmentObject)) {
alphaWidenedMask |= (1u << i);
}
if (IsIntegerColorAttachment(attachmentObject)) {
integerColorMask |= (1u << i);
}
}
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
g_integerColorDrawBufferMask = integerColorMask;
}
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
// only apply (and stamp the memo) when this object is bound as READ.
if (asTarget == FramebufferTarget::Read) {
SyncReadBufferToBackend(stateFBOObject);
}
// -------------------- Attach texture to backend FBO -----------------------
// A backend texture id was re-minted since this twin's last walk
// (RecreateBackendTexture): any point here may still hold the dead id while
// its frontend attachment version is unchanged, so the memo below would skip
// exactly the attachment that needs repair. Re-arm every point first.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
// P4a (D-C4): the record's ContentHash is what says this framebuffer's resolved state
// moved, and it REPLACES the frontend attachment versions AS A KEY. It covers every
// field the record carries, so an attachment set that moved, a draw-buffer array
// that moved, an extent that moved and a recycled Fbo whose successor happens to
// carry an identical attachment set are all one compare - and it moves in ONE place
// rather than in an array of 41 the twin had to walk.
//
// The re-arm is expressed through m_syncedFrontendAttachmentVersions rather than
// around it, so the attachment loop below is unchanged on both arms: a moved record
// arms every point and the loop then does exactly what it does today, including the
// empty-colour-point detach that keeps m_backendColorSlots a permutation of the
// PHYSICAL layout.
//
// OVER-FIRING IS FREE AND UNDER-FIRING IS FATAL, so the per-attachment versions stay
// as a second, narrower gate underneath: a frontend attachment that moved without
// the record moving still re-attaches. That direction is the safe one and it is the
// reason the array is re-armed rather than retired here. It is what the array is FOR
// now: the walk below resolves each attachment from the record (SyncAttachmentSurface),
// so the versions no longer say WHICH object a point holds - they say only "this point
// may have moved since I last looked", and they retire with the frontend attachment
// array itself, which is a later phase's.
if (pushedRecord != nullptr && m_syncedRecordHashes[SizeT(asTarget)] != pushedRecord->ContentHash) {
std::fill(m_syncedFrontendAttachmentVersions.begin(), m_syncedFrontendAttachmentVersions.end(),
static_cast<Uint16>(~0u));
m_syncedRecordHashes[SizeT(asTarget)] = pushedRecord->ContentHash;
}
const auto& attachments = stateFBOObject->GetAllAttachmentObjects();
const auto& attachmentVersions = stateFBOObject->GetAllFramebufferAttachmentVersions();
for (SizeT i = 0; i < attachments.size(); ++i) {
const auto& attachmentObject = attachments[i];
auto frontendType = static_cast<FramebufferAttachmentType>(i);
GLenum glBackendAttachment = GL_NONE;
if (frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31)
glBackendAttachment = GetBackendAttachmentType(frontendType);
else
glBackendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType);
// relevant FRONTEND!!! version should be checked and updated
if (m_syncedFrontendAttachmentVersions[i] != attachmentVersions[i]) {
// SyncAttachmentObject only ever attaches: for an empty frontend attachment it
// returns true and issues nothing, so the point keeps whatever was there. That is
// what makes m_backendColorSlots a permutation of the PHYSICAL layout rather than
// a claim about one - a point handed to an attachment with no image would
// otherwise still hold the previous owner's image and glReadBuffer would return
// it. Bounded by GL_MAX_COLOR_ATTACHMENTS because GL_COLOR_ATTACHMENTn above the
// driver's limit is INVALID_ENUM, and restricted to colour points because
// FRONT_LEFT/BACK_LEFT and co. are not ES attachment points at all.
const Bool isColorPoint =
frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31 &&
(static_cast<Int>(frontendType) - static_cast<Int>(FramebufferAttachmentType::Color0)) <
g_GLESCapabilities.MaxColorAttachments;
// P5e (fb, §5.4): ON THE RECORD ARM THE EMPTY POINT IS THE RECORD'S, and it
// is the SAME test SyncAttachmentSurface attaches on - `Kind == None ||
// Res null`. Asking the frontend here and the record there is what left
// review N-6's one-directional hole; one test closes it by construction.
const Bool pointIsEmpty =
pushedRecord != nullptr
? [&] {
const MG_Pipe::MGPSurface* s =
PushedSurfaceForAttachment(*pushedRecord, frontendType);
return s == nullptr || s->Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(s->Res);
}()
: attachmentObject.IsEmpty();
if (isColorPoint && pointIsEmpty && glBackendAttachment != GL_NONE) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
}
// M-3: the ATTACHMENT RESOLUTION is the record's on the handle arm. The walk
// itself still runs over the frontend's 41 points, because that is what says
// which points EXIST and it is also what carries the per-attachment version
// memo underneath the record hash; what the record answers is WHICH object
// each point holds and in what shape.
//
// A point the record does not describe (Color8..Color31; the FRONT/BACK
// tokens) is only reachable with a non-empty attachment if D-C3's refusal
// failed to fire, so an empty one is silently fine and a live one is loud.
Bool attachmentSynced;
if (pushedRecord != nullptr) {
const MG_Pipe::MGPSurface* pushedSurface =
PushedSurfaceForAttachment(*pushedRecord, frontendType);
if (pushedSurface == nullptr) {
if (attachmentObject.IsEmpty()) {
attachmentSynced = true;
} else {
MGLOG_E_ONCE("MGPipe: framebuffer %u holds an attachment at point %s, which "
"its record does not describe - refusing to attach it",
stateFBOObject->GetExternalIndex(),
MG_Util::ConvertFramebufferAttachmentTypeToString(frontendType).c_str());
attachmentSynced = false;
}
} else {
attachmentSynced =
SyncAttachmentSurface(glFBOTarget, *pushedSurface, glBackendAttachment,
&attachmentObject);
}
} else {
attachmentSynced = SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment);
}
if (attachmentSynced) {
m_syncedFrontendAttachmentVersions[i] = attachmentVersions[i];
}
}
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
else {
MGLOG_D("%s: Skipped SyncAttachmentObject(target=%s, frontendObj=(%dx%dx%d, %s), backendAtt=%s), "
"version = %u",
__func__, MG_Util::ConvertGLEnumToString(glFBOTarget).c_str(),
attachmentObject.GetSize().x(), attachmentObject.GetSize().y(),
attachmentObject.GetSize().z(),
MG_Util::ConvertFramebufferAttachmentTypeToString(frontendType).c_str(),
MG_Util::ConvertGLEnumToString(glBackendAttachment).c_str(),
m_syncedFrontendAttachmentVersions[i]);
if (!attachmentObject.IsTexture() && !attachmentObject.IsRenderbuffer()) {
continue;
}
GLint objectType = GL_NONE;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
MOBILEGL_ASSERT((objectType == GL_NONE) ||
(attachmentObject.IsTexture() && objectType == GL_TEXTURE) ||
(attachmentObject.IsRenderbuffer() && objectType == GL_RENDERBUFFER),
"Attachment type not match!");
GLint objectName = 0;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName);
// Verify that the backend object's name and parameters match the frontend attachment state
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
MOBILEGL_ASSERT(backendTextureSlot != nullptr && *backendTextureSlot != nullptr,
"No backend texture found while framebuffer reports texture attachment.");
GLuint backendTexId = (*backendTextureSlot)->GetBackendTextureId();
MOBILEGL_ASSERT(static_cast<GLint>(backendTexId) == objectName,
"Attachment texture name mismatch between GLES (%d) and backend texture object "
"(%d), frontend texture object ID=%d.",
objectName, backendTexId, textureObject->GetExternalIndex());
GLint texLevel = 0;
g_GLESFuncs.glGetFramebufferAttachmentParameteriv(
glFBOTarget, glBackendAttachment, GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &texLevel);
MOBILEGL_ASSERT(texLevel == static_cast<GLint>(attachmentObject.GetTextureLevel()),
"Attachment texture level mismatch between GLES and state object.");
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named
// debt inside the scope - P3b/P4b rekeys the registry.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
auto* backendRboSlot =
RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get());
MOBILEGL_ASSERT(
backendRboSlot != nullptr && *backendRboSlot != nullptr,
"No backend renderbuffer found while framebuffer reports renderbuffer attachment.");
GLuint backendRboId = (*backendRboSlot)->GetBackendRenderbufferId();
MOBILEGL_ASSERT(static_cast<GLint>(backendRboId) == objectName,
"Attachment renderbuffer name mismatch between GLES and state object.");
}
}
#endif
}
// The walk itself can re-mint an id (SyncAttachmentObject ->
// SyncMipmapsToBackend -> RecreateBackendTexture), invalidating points this
// walk already attached or version-skipped - e.g. one texture attached at two
// points. Re-enter until the generation is quiescent: every pass syncs each
// dirty texture clean, so each repeat finds strictly fewer re-mints and the
// common case (no re-mint) never takes a second pass. The head's draw/read-
// buffer syncs are memoized against their own shadows, so a repeat re-walks
// only the attachments.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
SyncToBackend(stateFBOObject, asTarget);
}
}
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------
//
// WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which
-15
View File
@@ -2324,8 +2324,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
~BackendFramebufferObject();
BackendFramebufferObject(const BackendFramebufferObject&) = delete;
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget);
// P5e SEAM (MG_Remote/CONTRACT-P5E.md §5.4; declared by c0e, bodied by fb): the same
// sync keyed on the framebuffer HANDLE. The record's eleven surfaces ARE the point
// set - the emitter refuses a point at or above the wire width - so the attachment
@@ -2333,10 +2331,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// handle is the argument. An OVERLOAD rather than a changed signature, so the
// monolith arm and the pull build see no token move.
void SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget asTarget);
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
// P5e (fb, §5.4): the same read-buffer push keyed on the handle, for the one path
// that applies a read buffer without doing the rest of the sync - SyncCurrentFBO's
// "one object is bound to BOTH bindings" skip, where the DRAW pass already did the
@@ -2514,15 +2508,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GL_CLAMP_READ_COLOR rule the clamp above implements.
Bool IsAlphaWidenedFallbackReadAttachment();
// True when this attachment's storage carries an alpha channel its frontend format does
// not (the three-channel colour-renderable widening).
Bool IsAlphaWidenedColorAttachment(const MG_State::GLState::FramebufferAttachmentObject& attachmentObject);
// Bit i set = DRAW BUFFER i of `fbo` resolves to a colour attachment the backend widened
// from three channels to four. Indexed by draw-buffer slot, not by attachment point,
// because that is what glColorMaski / glClearBufferfv address.
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo);
// The same mask for whatever is currently bound to GL_DRAW_FRAMEBUFFER, recomputed by
// SyncCurrentFBO (BackendFramebufferObject::SyncToBackend for the DRAW target, and reset
// to 0 on the default framebuffer). Read by the draw/clear state sync, so it is only
@@ -987,7 +987,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
@@ -1008,19 +1007,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.GetTextureImage = GetTextureImage;
funcsTable.GL.DispatchCompute = DispatchCompute;
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
funcsTable.GL.MemoryBarrier = MemoryBarrier;
@@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value);
}
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
}
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
DrawWireIndirect(mode, type, indirect, drawcount, stride, true);
return;
@@ -560,18 +532,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::ReadPixels called with null GL context");
pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels);
}
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::GetTexImage called with null GL context");
pVulkanRenderer->GetTexImage(target, level, format, type, pixels);
}
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::GetTextureImage called with null GL context");
pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels);
}
void Clear(GLbitfield mask) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::Clear called with null GL context");
@@ -780,13 +740,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->MultiDrawElements(payload);
}
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::MultiDrawElements called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "DirectVulkan::DrawElementsBaseVertex called with null GL context");
@@ -824,15 +777,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
}
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitNamedFramebuffer called with null VulkanRenderer");
pVulkanRenderer->BlitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0,
dstY0, dstX1, dstY1, mask, filter);
}
namespace {
// Backend fence handle: the queue-submission index captured at fence
// creation (see VulkanRenderer::GetSyncPointSubmitIndex). The fence is
@@ -65,21 +65,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
@@ -105,11 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
@@ -129,9 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
// GL fence sync objects, mapped onto the renderer's frame-serial busy
// tracking: a fence captures the frame serial current at creation and is
// signaled once every command recorded under that serial has completed on
@@ -602,7 +602,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
}
@@ -689,60 +688,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
// The rebind memo's set may be among the freed ones.
m_fastRebindMemo.valid = false;
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 binding, Uint32 element,
VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint) const {
return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo);
}
Bool UniformManager::ResolveSamplerDescriptorOverride(
const SamplerBindingOverride& samplerBindingOverride, VkDescriptorImageInfo& outImageInfo) const {
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptorOverride: texture manager is null");
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptorOverride: sampler manager is null");
MOBILEGL_ASSERT(samplerBindingOverride.texture != nullptr,
"ResolveSamplerDescriptorOverride: override texture is null for binding %u",
samplerBindingOverride.binding);
MOBILEGL_ASSERT(samplerBindingOverride.sampler != nullptr,
"ResolveSamplerDescriptorOverride: override sampler is null for binding %u",
samplerBindingOverride.binding);
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*samplerBindingOverride.texture);
MOBILEGL_ASSERT(resource != nullptr,
"ResolveSamplerDescriptorOverride: failed to sync override texture resource for binding %u textureId=%d",
samplerBindingOverride.binding, samplerBindingOverride.texture->GetExternalIndex());
MOBILEGL_ASSERT(IsValidSampledImageLayout(resource->layout),
"ResolveSamplerDescriptorOverride: invalid layout %d for binding %u textureId=%d",
static_cast<Int>(resource->layout), samplerBindingOverride.binding,
samplerBindingOverride.texture->GetExternalIndex());
outImageInfo = {
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
*samplerBindingOverride.texture,
samplerBindingOverride.forceNearestFiltering,
resource->sampledLevelCount),
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
samplerBindingOverride.imageView :
// Same reason as in ResolveSamplerDescriptor: the resource's own views describe
// the storage texture, so a view has to be asked for its own.
(samplerBindingOverride.texture->IsTextureView()
? m_textureManager->GetOrCreateSampledImageView(*samplerBindingOverride.texture,
VK_FORMAT_UNDEFINED)
: (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView)),
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
samplerBindingOverride.imageLayout : resource->layout,
};
return outImageInfo.sampler != VK_NULL_HANDLE;
}
Bool UniformManager::ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex,
Bool storage, VkBufferView& out) {
@@ -1553,10 +1502,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint,
const SamplerBindingOverride* samplerBindingOverride,
Bool samplerDescriptorsUnchangedHint,
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
VkPipelineBindPoint bindPoint) {
// This program has a descriptor MobileGL could not resolve (see
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
// binding is still declared in the layout, so the pipeline is consistent with the shader
@@ -1577,39 +1523,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
}
// Dynamic-offset-only rebind (see FastRebindMemo in the header): the last
// cacheable walk of this exact program selected a set whose contents are
// provably still what this walk would write - the hint covers every
// sampler binding, and an unchanged (buffer, range) for the single
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
// the SAME set delivers without any descriptor write.
const Bool cacheable = samplerBindingOverride == nullptr &&
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
m_fastRebindMemo.frameIndex == frameIndex &&
// A wire store destroyed since the memo was taken may have handed its handle
// value to a later mint (see FastRebindMemo): "same VkBuffer" then names a
// different store, so the memo is refused and the full walk re-records it.
m_fastRebindMemo.wireStoreDestroyEpoch == m_bufferManager->GetWireStoreDestroyEpoch() &&
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
m_fastRebindMemo.programHash == programObj.hash) {
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
Uint32 uboDynamicOffset = 0;
if (ResolveDynamicUboDescriptor(program, programObj, m_fastRebindMemo.uboBinding, 0, frameIndex,
uboBuffer, uboRange, uboDynamicOffset) &&
uboBuffer == m_fastRebindMemo.uboBuffer && uboRange == m_fastRebindMemo.uboRange) {
auto& fastOffsets = m_dynamicOffsetsScratch;
fastOffsets.clear();
fastOffsets.push_back(uboDynamicOffset);
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout,
m_fastRebindMemo.set, fastOffsets);
return true;
}
// Any mismatch (arena wrap or growth, direct-bind retarget, upload
// failure) falls through to the full walk, which re-records the memo.
}
// The descriptor set is chosen AFTER the writes are built (below), so a draw
// whose resolved descriptor content matches the previous draw can reuse that
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
@@ -1660,13 +1573,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
// Eligibility probe for FastRebindMemo, filled by this walk: exactly one
// dynamic-UBO descriptor (no arrayed elements) and otherwise only
// combined-image samplers, so the whole set's content is pinned by the
// sampler hint plus one (buffer, range) compare.
Uint32 dynamicUboDescriptorCount = 0;
Uint32 fastRebindUboBinding = 0;
Bool fastRebindKindsEligible = true;
// Iterate only the bindings this program declares. The old walk covered all 256 slots of
// bindingKinds on every draw to find the 1-8 a real program uses.
@@ -1685,8 +1591,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
dynamicUboDescriptorCount += descriptorCount;
fastRebindUboBinding = binding;
const SizeT firstBufferInfoIndex = bufferInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorBufferInfo bufferInfo{};
@@ -1719,7 +1623,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
texelBufferViews.push_back(bufferView);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
@@ -1738,7 +1641,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
texelBufferViews.push_back(bufferView);
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
write.pTexelBufferView = &texelBufferViews.back();
writes.push_back(write);
@@ -1760,7 +1662,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bufferInfos.push_back(bufferInfo);
}
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write.descriptorCount = descriptorCount;
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
@@ -1786,7 +1687,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
imageInfos.push_back(imageInfo);
}
fastRebindKindsEligible = false;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
write.descriptorCount = descriptorCount;
write.pImageInfo = &imageInfos[firstImageInfoIndex];
@@ -1798,37 +1698,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// so a shader indexing them sampled a descriptor nobody had filled in
// (KHR-GL42.shading_language_420pack.binding_sampler_array).
const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding);
// Overrides come only from MobileGL's own blit and depth-mipmap programs, whose
// samplers are scalars; the override replaces THE descriptor at its binding, so
// there is no element for it to mean on an arrayed one.
const Bool overrideThisBinding = samplerBindingOverride != nullptr &&
samplerBindingOverride->binding == binding &&
samplerBindingOverride->texture != nullptr &&
samplerBindingOverride->sampler != nullptr;
MOBILEGL_ASSERT(
!overrideThisBinding || descriptorCount == 1,
"BindProgramUniformBuffers: sampler override targets arrayed binding %u (%u descriptors)",
binding, descriptorCount);
const SizeT firstImageInfoIndex = imageInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
VkDescriptorImageInfo imageInfo{};
const SamplerBindingOverride* overrideForElement =
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
const auto overrideIt = std::find_if(
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
[binding, element](const SamplerBindingOverride& candidate) {
return candidate.binding == binding && candidate.element == element;
});
if (overrideIt != samplerBindingOverrides->end()) {
overrideForElement = &*overrideIt;
}
}
const Bool hasImage = overrideForElement != nullptr
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
element, imageInfo,
samplerDescriptorsUnchangedHint);
const Bool hasImage = ResolveWireImageDescriptor(commandBuffer, program, programObj, binding,
element, false, imageInfo);
if (!hasImage) {
MGLOG_E_ONCE(
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
@@ -1845,14 +1719,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
imageInfos.push_back(imageInfo);
}
if (descriptorCount > 1) {
// The dynamic-offset-only rebind replays a whole descriptor set on the
// strength of the sampler hint alone, and its eligibility probe was written
// for bindings that carry one descriptor each. An arrayed sampler binding
// also bypasses the per-binding descriptor memo, so there is nothing for it
// to win here either.
fastRebindKindsEligible = false;
}
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.descriptorCount = descriptorCount;
write.pImageInfo = &imageInfos[firstImageInfoIndex];
@@ -1906,12 +1772,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkDescriptorSet reusedSet = VK_NULL_HANDLE;
if (cacheable) {
for (const auto& entry : m_descriptorReuseMemo) {
if (entry.valid && entry.signature == signature) {
reusedSet = entry.set;
break;
}
for (const auto& entry : m_descriptorReuseMemo) {
if (entry.valid && entry.signature == signature) {
reusedSet = entry.set;
break;
}
}
if (reusedSet != VK_NULL_HANDLE) {
@@ -1929,31 +1793,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!writes.empty()) {
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
}
if (cacheable) {
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
DescriptorReuseEntry{signature, descriptorSet, true};
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
} else {
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
}
}
// (Re)record the dynamic-offset-only rebind memo. Recording on every
// cacheable walk (allocated or reused set alike - both hold exactly the
// content just computed) keeps the single slot tracking the most recent
// program; a non-cacheable override walk drops it alongside the reuse
// memo above.
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
m_fastRebindMemo = FastRebindMemo{
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
fastRebindUboBinding, bufferInfos[0].buffer,
bufferInfos[0].range, descriptorSet,
m_bufferManager->GetWireStoreDestroyEpoch(),
};
} else {
m_fastRebindMemo.valid = false;
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
DescriptorReuseEntry{signature, descriptorSet, true};
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
}
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet,
@@ -27,16 +27,6 @@ namespace MobileGL::MG_State::GLState {
namespace MobileGL::MG_Backend::DirectVulkan {
class UniformManager {
public:
struct SamplerBindingOverride {
Uint32 binding = 0;
Uint32 element = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Bool forceNearestFiltering = false;
};
struct SamplerImageFeedbackBinding {
Uint32 samplerBinding = 0;
Uint32 samplerElement = 0;
@@ -107,20 +97,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// submit older work, but descriptor recording must never rotate it.
Bool PrepareWireTextureResources(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params
// sum, sampling-resolution generation (sampler params + texture shape), image
// epochs AND per-resource layout values - so the per-binding cached
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false,
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -277,18 +258,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// class when the device cannot use the declared one as a texel buffer.
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
Bool storage);
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
// independently assigned GL texture unit, so it selects the texture, the sampler
// override and the fallback separately from its neighbours.
//
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries). The cache is keyed by binding alone, so it is
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const;
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
@@ -301,8 +270,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0};
VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0};
Uint32 m_wireMaxTexelElements = ~Uint32{0};
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
@@ -440,47 +407,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
// descriptor set selected by the last cacheable full walk of a program
// whose active bindings are exactly one dynamic UBO (single descriptor)
// plus combined-image samplers. When the next call proves every sampler
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
// moved, the per-draw glUniform case - the walk collapses to: resolve one
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
// rebinding the same set with different dynamic offsets).
// Invalidation inventory: BeginFrame clears it (the frame's sets are
// recycled) and the frameIndex field guards cross-frame confusion on top;
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
// program relink bumps the backend state version and thus programObj.hash
// so the key misses; the program lifetime id is never reused, so a
// deleted-and-recreated program misses; a texture/sampler/binding change
// drops the hint upstream; an arena wrap or growth resolves a different
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
// advances, so the recorded set is never re-written within its frame.
// P7 M2 round 2 (ID-P7-43): a WIRE store can die MID-FRAME on the reclaim
// path (VkBufferManager::DeferredWireRelease), and the next mint can hand
// its VkBuffer handle value back - a heap pointer under lavapipe - so "the
// same VkBuffer+range" is no longer proof of the same store. The memo
// therefore records VkBufferManager::GetWireStoreDestroyEpoch() and misses
// once any wire store has been destroyed since; the descriptor-reuse
// signature below folds the same epoch in. On the wire arm this memo is
// never consulted (SetupWireDraw passes no sampler hint), so the field is
// the defensive half; the signature is the half the wire arm reaches.
struct FastRebindMemo {
Bool valid = false;
Uint32 frameIndex = 0;
Uint64 programLifetimeId = 0;
ProgramFactory::HashType programHash = 0;
Uint32 uboBinding = 0;
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Uint64 wireStoreDestroyEpoch = 0;
};
FastRebindMemo m_fastRebindMemo;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
@@ -68,24 +68,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
toLinear(payload.color.z()), payload.color.w());
}
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
break;
case ClearColorEncoding::Uint:
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
break;
case ClearColorEncoding::Float:
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
break;
}
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) {
if (!IsCubeMapFaceUploadTarget(target)) {
return 0;
@@ -93,18 +75,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
static Uint32 ResolveAttachmentBaseArrayLayer(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return 0;
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
}
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
// not a cosmetic difference: the count below is not key-only, it is written straight into
@@ -113,65 +83,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// for good, while the very same queued clear cleared all six through the render pass's
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
if (attachmentType == FramebufferAttachmentType::None) {
return nullptr;
}
const auto& attachment = drawFbo.GetAttachment(attachmentType);
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
return nullptr;
}
return &attachment;
}
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
// by - the storage texture; keying it on the view instead left the clear invisible to every
// materialisation done through the parent's name (and vice versa), so the image stayed in
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return nullptr;
}
const auto& storageOwner = texture->GetViewStorageOwner();
return storageOwner ? storageOwner.get() : texture;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) {
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
if (rawTexture != nullptr && texture != rawTexture) {
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
// of a view's level 0 would collide with a clear of the storage's level 0 even when
// the view opened onto level 1.
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
}
return PendingClearKey {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
};
}
PendingClearKey VkClearManager::MakePendingClearKey(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
MOBILEGL_ASSERT(attachment.IsTexture() && !attachment.IsRenderbuffer(),
"MakePendingClearKey requires a texture framebuffer attachment");
auto* texture = attachment.GetTexture().get();
MOBILEGL_ASSERT(texture != nullptr, "MakePendingClearKey: texture attachment resolved to null");
const Uint32 mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
const Uint32 layerCount = ResolveAttachmentLayerCount(attachment);
return MakePendingClearKey(texture, mipLevel, baseArrayLayer, layerCount);
}
Bool VkClearManager::Initialize() {
return true;
}
@@ -183,355 +94,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
// the storage it views. A clear posted against a view and one posted against its parent
// target the same image, so they have to coalesce rather than queue independently.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
};
}
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask;
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
// The whole colour story travels together (same rule as
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
// payload carries its value in colorInt/colorUint and its branch selector
// in colorEncoding - dropping them here would leave the pending clear
// reading as an all-zero float one.
dst.color = src.color;
dst.colorEncoding = src.colorEncoding;
dst.colorInt = src.colorInt;
dst.colorUint = src.colorUint;
}
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth;
}
if ((src.mask & GL_STENCIL_BUFFER_BIT) != 0) {
dst.stencil = src.stencil;
}
}
void VkClearManager::ErasePendingClearsForTextureLocked(const TextureIdentity& identity) {
Vector<PendingClearKey> keysToErase;
keysToErase.reserve(m_pendingClears.size());
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (PendingClearMatchesTextureIdentity(it->first, identity)) {
keysToErase.emplace_back(it->first);
}
}
for (const auto& key : keysToErase) {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
if (identity.texture == nullptr) {
return false;
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt == m_aliveObjects.end()) {
ErasePendingClearsForTextureLocked(identity);
return false;
}
outTexture = aliveIt->second.lock();
if (!outTexture || outTexture.get() != identity.texture || outTexture->GetLifetimeId() != identity.lifetimeId) {
ErasePendingClearsForTextureLocked(identity);
outTexture.reset();
return false;
}
return true;
}
Bool VkClearManager::LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
return LockTextureIdentityLocked(TextureIdentity{
.texture = key.texture,
.lifetimeId = key.textureLifetimeId,
}, outTexture);
}
void VkClearManager::QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) {
if (mask & GL_COLOR_BUFFER_BIT) {
auto& drawbufs = drawFbo.GetDrawBuffers();
// This should automatically work on default & offscreen FBO
for (auto drawbuf: drawbufs) {
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
if (!attachment) {
continue;
}
QueueClear({
.mask = GL_COLOR_BUFFER_BIT,
.color = clearPayload.color
}, *attachment);
MGLOG_D("%s: %s (texture %d) - color = (%.2f, %.2f, %.2f, %.2f)", __func__,
MG_Util::ConvertFramebufferAttachmentTypeToString(drawbuf).c_str(),
attachment->GetTexture()->GetExternalIndex(),
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
}
}
if (mask & GL_DEPTH_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Depth);
if (attachment) {
QueueClear({
.mask = GL_DEPTH_BUFFER_BIT,
.depth = clearPayload.depth,
}, *attachment);
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.depth);
}
}
if (mask & GL_STENCIL_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Stencil);
if (attachment) {
QueueClear({
.mask = GL_STENCIL_BUFFER_BIT,
.stencil = clearPayload.stencil,
}, *attachment);
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.stencil);
}
}
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (clearPayload.mask == 0 || !texture) {
return;
}
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (clearPayload.mask == 0 || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
return;
}
const auto texture = attachment.GetTexture();
if (!texture) {
return;
}
const PendingClearKey key = MakePendingClearKey(attachment);
// The alive entry must hold the STORAGE object, because the key names it:
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
// storage's identity made every lookup of this clear fail that check and silently report
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(it->first, liveTexture);
}
}
return false;
}
Bool VkClearManager::HasPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
return false;
}
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(key, liveTexture);
}
Bool VkClearManager::HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return false;
}
return HasPendingClear(MakePendingClearKey(attachment));
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return GetPendingClear(key, outPayload, liveTexture);
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
return false;
}
auto it = m_pendingClears.find(key);
if (it == m_pendingClears.end()) {
outTexture.reset();
return false;
}
outPayload = it->second;
MGLOG_D("%s: Got pending clear for texture@%p lifetime=%llu, mip=%u layer=%u count=%u mask=0x%x clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
static_cast<void*>(key.texture),
static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount,
static_cast<Uint32>(outPayload.mask),
outPayload.color[0], outPayload.color[1], outPayload.color[2], outPayload.color[3],
outPayload.depth,
outPayload.stencil);
return true;
}
Bool VkClearManager::GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
MGLOG_D("%s: Failed getting pending clear for non-texture framebuffer attachment", __func__);
return false;
}
return GetPendingClear(MakePendingClearKey(attachment), outPayload);
}
Bool VkClearManager::GetPendingClears(MG_State::GLState::ITextureObject* texture,
Vector<PendingClearEntry>& outEntries) {
outEntries.clear();
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (!LockTextureIdentityLocked(MakeTextureIdentity(texture), liveTexture)) {
return false;
}
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
outEntries.emplace_back(PendingClearEntry{.key = it->first, .payload = it->second});
}
}
return !outEntries.empty();
}
void VkClearManager::PopPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
ErasePendingClearsForTextureLocked(identity);
}
void VkClearManager::PopPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return;
}
{
const std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
MGLOG_D("%s: Pop pending clear for texture@%p lifetime=%llu mip=%u layer=%u count=%u", __func__,
static_cast<void*>(key.texture), static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount);
}
void VkClearManager::PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return;
}
PopPendingClear(MakePendingClearKey(attachment));
}
SizeT VkClearManager::CollectGarbage() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
Vector<TextureIdentity> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->second.expired()) {
expiredTextures.emplace_back(it->first);
}
}
if (expiredTextures.empty()) {
return 0;
}
for (const auto& identity : expiredTextures) {
ErasePendingClearsForTextureLocked(identity);
}
return expiredTextures.size();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -46,10 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// expressed in whichever type matches (GL 4.6 core 15.2.3).
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
// Applies that same rule in place, for the paths that have to bake it into the payload before
// the destination is known.
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
// function to whatever value it is handed. Every other write path in this backend goes through
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
@@ -113,42 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
// Resolves a GL texture view to the storage it views before keying; see the definition.
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize();
void Shutdown();
void QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo);
void QueueClear(
const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture);
void QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool HasPendingClear(MG_State::GLState::ITextureObject* texture);
Bool HasPendingClear(const PendingClearKey& key);
Bool HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload);
Bool GetPendingClears(MG_State::GLState::ITextureObject* texture, Vector<PendingClearEntry>& outEntries);
void PopPendingClear(MG_State::GLState::ITextureObject* texture);
void PopPendingClear(const PendingClearKey& key);
void PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
SizeT CollectGarbage();
private:
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
static void MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src);
void ErasePendingClearsForTextureLocked(const TextureIdentity& identity);
Bool LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
File diff suppressed because it is too large Load Diff
@@ -232,51 +232,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// targets): a default-framebuffer pass of one surface is not another's.
void SetDefaultFramebufferTarget(Uint64 serial) {
m_defaultFramebufferTarget = serial;
m_rpFastValid = false;
}
// Destroys every cached pass and framebuffer (not the renderbuffers). The device must be
// idle and no pass may be open: a surface target's image views are going away.
void PurgeRenderPasses();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along).
//
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
// all - a texture the texture manager declined to back (an unsupported format or sample
// count), or an attachment view it cannot construct (a layer span the image has no room
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
// unrepresentable: the function returned a reference, so the only thing the two fallible
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
//
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
// already drops a draw whose sampler descriptor could not be resolved
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
// themselves, so a caller needs no message of its own.
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
// Frame boundary hook: ages the render-pass cache and evicts long-unused
// entries (their command buffers retired many frames ago).
void OnPresent();
@@ -308,28 +268,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
// version change, swapchain rotation, any attachment image recreation (the two epochs),
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Uint64 m_defaultFramebufferTarget = 0;
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
// a deleted FBO reallocated at the same address whose fresh setup performed
// the same number of version bumps would otherwise compare equal (both count
// from 0), serving the dead framebuffer's pass to the new object.
Uint64 m_rpFastFboLifetimeId = 0;
Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
@@ -359,13 +298,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
@@ -414,8 +346,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
// Frame-boundary margin after which a resource last referenced by a retired
// GL object (or superseded backing) is provably past every in-flight frame.
@@ -440,12 +440,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return vkSampler;
}
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
}
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
@@ -40,12 +40,6 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// The applier owns these values: effective bound/builtin sampler parameters and
// the sampled texture view's format/range. No frontend object is constructed.
VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
@@ -866,271 +866,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return resourcePtr;
}
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view shares this resource with the texture it views, so it must not touch
// perMipViews: that vector is indexed by mip level alone and holds views built with the
// STORAGE texture's format and full layer range. Route it through the keyed attachment
// cache instead, where its own window is part of the key.
if (texture.IsTextureView()) {
const TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
return GetOrCreateAttachmentViewAtMipLevel(texture, mipLevel, window.baseArrayLayer, window.layerCount,
window.viewType);
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (resource->perMipViews.size() != resource->mipLevels) {
resource->perMipViews.resize(resource->mipLevels, VK_NULL_HANDLE);
}
VkImageView& perMipView = resource->perMipViews[mipLevel];
if (perMipView != VK_NULL_HANDLE) {
return perMipView;
}
perMipView = CreateImageView(resource->image, resource->format, resource->aspect, resource->viewType,
mipLevel, 1, 0, resource->arrayLayers);
if (perMipView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u", __func__, texture.GetExternalIndex(), mipLevel);
return VK_NULL_HANDLE;
}
return perMipView;
}
VkImageView VkTextureManager::GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, Uint32 baseArrayLayer,
Uint32 layerCount,
VkImageViewType viewType) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// mipLevel and baseArrayLayer arrive in STORAGE space - every caller runs them through
// ToStorageMipLevel / ToStorageArrayLayer at the GL attachment boundary. What a GL texture
// view still contributes here is its own internal format, which may reinterpret the
// storage's (GL 4.6 core table 8.21) and is what the attachment must actually be written
// through.
VkFormat viewFormatOverride = VK_FORMAT_UNDEFINED;
if (texture.IsTextureView()) {
viewFormatOverride = ResolveTextureViewWindow(texture, *resource).format;
}
if (mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
// means). BOTH spellings are legal only on a 2D-array-compatible image
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
// may have had refused per format.
//
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
// Not an error line: the render-pass builder turns the null view into one
// MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
"slices, 2D-array-compatible=%d)",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
static_cast<Int>(viewType), mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE;
}
} else if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
resource->arrayLayers);
return VK_NULL_HANDLE;
}
const Bool framebufferSrgbEnabled =
MG_Pipe::gPipeInputs.IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat baseAttachmentFormat =
viewFormatOverride != VK_FORMAT_UNDEFINED ? viewFormatOverride : resource->format;
const VkFormat attachmentFormat =
ResolveSrgbAttachmentWriteFormat(baseAttachmentFormat, framebufferSrgbEnabled);
// The shortcut back to the per-mip vector is only sound for the storage texture itself;
// for a view every field below is part of what distinguishes it from its parent.
if (viewFormatOverride == VK_FORMAT_UNDEFINED && attachmentFormat == resource->format &&
baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
const TextureResource::AttachmentViewKey key{
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
.viewFormat = attachmentFormat,
};
auto it = resource->attachmentViews.find(key);
if (it == resource->attachmentViews.end()) {
it = resource->attachmentViews.emplace(key, VK_NULL_HANDLE).first;
}
VkImageView& attachmentView = it->second;
if (attachmentView != VK_NULL_HANDLE) {
return attachmentView;
}
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
mipLevel, 1, baseArrayLayer, layerCount);
if (attachmentView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
__func__, texture.GetExternalIndex(), mipLevel, baseArrayLayer, layerCount, static_cast<Int>(viewType));
resource->attachmentViews.erase(it);
return VK_NULL_HANDLE;
}
return attachmentView;
}
// Builds (and caches) one sampled VkImageView over `resource`'s image for an arbitrary
// window - the shared back end of every GL-texture-view sampled path. Keyed by the whole
// window, which is what keeps a D24S8's depth-aspect view and its stencil-aspect view apart
// in the same cache while both name the same image, the same levels and the same layers.
VkImageView VkTextureManager::GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource,
const TextureViewWindow& window) {
const TextureResource::SampledImageViewKey key{
.baseMipLevel = window.baseMipLevel,
.levelCount = window.levelCount,
.baseArrayLayer = window.baseArrayLayer,
.layerCount = window.layerCount,
.viewType = window.viewType,
.format = window.format,
.aspect = window.sampledAspect,
.componentSwizzle = PackComponentSwizzle(window.components),
};
const auto existing = resource.alternateSampledViews.find(key);
if (existing != resource.alternateSampledViews.end()) {
return existing->second;
}
if (window.format != resource.format &&
(resource.imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("%s: textureId=%d needs a mutable-format image to be viewed as format=%d "
"(image format=%d)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Int>(resource.format));
return VK_NULL_HANDLE;
}
const VkImageView view =
CreateImageView(resource.image, window.format, window.sampledAspect, window.viewType,
window.baseMipLevel, window.levelCount, window.baseArrayLayer, window.layerCount,
&window.components);
if (view == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to create sampled view for textureId=%d format=%d aspect=0x%x "
"mips=[%u,%u) layers=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(window.format),
static_cast<Uint32>(window.sampledAspect), window.baseMipLevel,
window.baseMipLevel + window.levelCount, window.baseArrayLayer,
window.baseArrayLayer + window.layerCount);
return VK_NULL_HANDLE;
}
resource.alternateSampledViews.emplace(key, view);
return view;
}
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
// A GL texture view never has a sampledView of its own on this resource - that one
// belongs to the storage texture, with the storage texture's format, level range and
// depth/stencil aspect. The window is the view's whole identity, so it always goes to the
// keyed cache, even when the requested format happens to match the image's.
if (texture.IsTextureView()) {
TextureViewWindow window = ResolveTextureViewWindow(texture, *resource);
if (format != VK_FORMAT_UNDEFINED) {
window.format = format;
}
return GetOrCreateWindowedSampledView(texture, *resource, window);
}
if (resource->sampledView == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
return resource->sampledView;
}
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E_ONCE("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
const TextureResource::SampledImageViewKey key{
.baseMipLevel = resource->sampledBaseMipLevel,
.levelCount = resource->sampledLevelCount,
.baseArrayLayer = 0,
.layerCount = resource->arrayLayers,
.viewType = resource->viewType,
.format = format,
.aspect = VK_IMAGE_ASPECT_COLOR_BIT,
.componentSwizzle = PackComponentSwizzle(
ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()))),
};
const auto existing = resource->alternateSampledViews.find(key);
if (existing != resource->alternateSampledViews.end()) {
return existing->second;
}
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
MGLOG_E_ONCE("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
"for textureId=%d (available=0x%x)",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
return VK_NULL_HANDLE;
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageView view = CreateImageView(
resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType,
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E_ONCE("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
resource->alternateSampledViews.emplace(key, view);
MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format), resource->sampledBaseMipLevel,
resource->sampledBaseMipLevel + resource->sampledLevelCount);
return view;
}
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
@@ -1141,32 +876,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkTextureManager::TextureResource* VkTextureManager::FindTextureResource(
MG_State::GLState::ITextureObject& textureOrView) {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() ? &it->second : nullptr;
}
Bool VkTextureManager::HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE;
}
Bool VkTextureManager::HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const {
auto& storage = StorageTextureOf(textureOrView);
auto it = m_textureResources.find(MakeTextureIdentity(&storage));
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
return it->second.syncedShapeVersion == storage.GetShapeVersion() &&
it->second.syncedContentVersion != storage.GetContentVersion();
}
Bool VkTextureManager::WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.lastRecordingWriteGeneration == m_recordingGeneration;
}
void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
@@ -3598,90 +3307,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return storageOwner ? *storageOwner : texture;
}
// The VkImageViewType a GL texture view's own target asks for. Deliberately derived from the
// GL target rather than inherited from the storage image: a 2D view of a 2D-array texture is
// a VK_IMAGE_VIEW_TYPE_2D over one layer, and a cube view of the same image is a
// VK_IMAGE_VIEW_TYPE_CUBE over six - which is the whole reason table 8.20 lists those pairs.
static VkImageViewType ResolveTextureViewImageViewType(TextureTarget target,
VkImageViewType storageViewType) {
switch (target) {
case TextureTarget::Texture1D:
return VK_IMAGE_VIEW_TYPE_1D;
case TextureTarget::Texture1DArray:
return VK_IMAGE_VIEW_TYPE_1D_ARRAY;
case TextureTarget::Texture2D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return VK_IMAGE_VIEW_TYPE_2D;
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisampleArray:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
case TextureTarget::TextureCubeMap:
return VK_IMAGE_VIEW_TYPE_CUBE;
case TextureTarget::TextureCubeMapArray:
return VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
default:
return storageViewType;
}
}
VkTextureManager::TextureViewWindow VkTextureManager::ResolveTextureViewWindow(
MG_State::GLState::ITextureObject& texture, const TextureResource& resource) const {
TextureViewWindow window{};
window.format = resource.format;
window.viewType = resource.viewType;
window.baseArrayLayer = 0;
window.layerCount = resource.arrayLayers;
window.sampledAspect =
ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode());
window.components = ResolveSampledViewComponents(texture, ResolveTextureFormatInfo(texture.GetFormat()));
ResolveViewMipRange(texture, resource.mipLevels, window.baseMipLevel, window.levelCount);
if (!texture.IsTextureView()) {
return window;
}
window.isTextureView = true;
// GL 4.6 core 8.18: the view's TEXTURE_BASE_LEVEL / TEXTURE_MAX_LEVEL are relative to the
// view, so ResolveViewMipRange above already clamped them against the view's own level
// count (TextureObjectView reports it); shifting by TEXTURE_VIEW_MIN_LEVEL puts them back
// into the storage image's numbering.
window.baseMipLevel += static_cast<Uint32>(texture.GetViewMinLevel());
window.baseArrayLayer = static_cast<Uint32>(texture.GetViewMinLayer());
window.layerCount = static_cast<Uint32>(texture.GetViewNumLayers());
window.viewType = ResolveTextureViewImageViewType(texture.GetTarget(), resource.viewType);
// The view's OWN internalformat, which may reinterpret the storage's (table 8.21).
const VkFormat viewFormat = ResolveTextureFormatInfo(texture.GetFormat()).format;
if (viewFormat != VK_FORMAT_UNDEFINED) {
window.format = viewFormat;
}
// Recomputed against the view's own format: a depth/stencil storage viewed as
// depth/stencil still has to honour the VIEW's DEPTH_STENCIL_TEXTURE_MODE, which is the
// one parameter Better Clouds deliberately sets differently on the two names.
window.sampledAspect =
ResolveSampledImageViewAspectMask(GetAspectMaskForFormat(window.format) != VK_IMAGE_ASPECT_NONE
? GetAspectMaskForFormat(window.format)
: resource.aspect,
texture.GetDepthStencilTextureMode());
// Clamp to what the image actually has; a malformed view must degrade to an empty range
// rather than reach vkCreateImageView with an out-of-bounds subresource.
if (window.baseMipLevel >= resource.mipLevels) {
window.baseMipLevel = resource.mipLevels - 1;
window.levelCount = 1;
} else {
window.levelCount = std::min(window.levelCount, resource.mipLevels - window.baseMipLevel);
}
if (window.levelCount == 0) window.levelCount = 1;
if (window.baseArrayLayer >= resource.arrayLayers) {
window.baseArrayLayer = resource.arrayLayers - 1;
window.layerCount = 1;
} else {
window.layerCount = std::min(window.layerCount, resource.arrayLayers - window.baseArrayLayer);
}
if (window.layerCount == 0) window.layerCount = 1;
return window;
}
// The extra VkImageCreateFlags a GL texture view needs on the image it views. Recorded
// BEFORE the storage texture is synced (see SyncTextureAndGetDescriptor) so the very first
// resolve of a view already creates - or recreates and copies forward - an image the view can
@@ -565,14 +565,6 @@ public:
Bool isTextureView = false;
};
// The four component swizzles packed into one value, for the sampled-view cache key.
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
}
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
const TextureResource& resource) const;
// Records what a GL texture view needs of the image it views, so the next sync of the
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
@@ -583,11 +575,6 @@ public:
// VkImageFormatListCreateInfo the image is created with.
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
Vector<VkFormat>& outFormats) const;
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
// image. Shared back end of every GL-texture-view sampled path.
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource, const TextureViewWindow& window);
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
// P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport
@@ -664,11 +651,6 @@ public:
const auto it = m_wireTextureResources.find(key);
return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr;
}
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType);
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture,
Uint32 writtenMipLevel,
@@ -693,20 +675,6 @@ public:
resource.lastRecordingWriteGeneration = m_recordingGeneration;
}
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
//
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
// before that write, which then overwrites the newer texels. (An image the recording only
// read keeps the old trade: its earlier reads see the newer texels.)
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
// The next sync would record client texels into the image that is live now: same shape (a
// shape change mints a new image instead) and content moved since the last sync.
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
// The live resource, or null.
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
// `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
// anything for an image that carries both aspects. Defaulted so the call sites that have no
// texture in hand keep the depth-aspect answer they have always given.
File diff suppressed because it is too large Load Diff
@@ -243,38 +243,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr);
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
const RenderPassEntry& compatibleRenderPassEntry);
enum class ScissoredClearPrep {
NotNeeded, // scissor covers the whole target — take the deferred whole-surface path instead
NoOp, // nothing to clear (degenerate target or empty scissor rect)
Ready, // a render pass is active; record vkCmdClearAttachments with the returned rect
};
ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
VkClearRect& outClearRect);
void Clear(GLbitfield mask);
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
@@ -311,16 +292,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
// One depth/stencil copy whose source and destination are the same format and whose region
// is a same-size, same-layer rectangle: the aspect named by `aspect` is staged through a
// device-local buffer so the destination's other aspect cannot be overwritten by the
// packed native word a whole-image copy moves.
Bool CopyDepthStencilAspectThroughBuffer(FrameContext::FrameData& frame, VkImage srcImage,
VkImage dstImage, VkFormat format, Uint32 srcMipLevel,
Uint32 srcBaseArrayLayer, Uint32 dstMipLevel,
Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
GLint dstY, GLsizei width, GLsizei height,
VkImageAspectFlagBits aspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
// Quarter-turn surface transforms swap the copy extent's axes.
@@ -338,10 +309,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
GLsizei bufSize, GLvoid* pixels);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
@@ -354,8 +321,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const PhysicalDevice& GetPhysicalDevice() const;
VkInstance GetInstance() const;
Bool IsDrawIndirectCountExtensionEnabled() const;
// GL fence support, expressed in queue-submission indices backed by
// real VkFences. A GL fence captures GetSyncPointSubmitIndex() at
// creation: the index of the submission that will carry the commands
@@ -578,11 +543,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
MultisampleResolveScratchImage m_msResolveScratch;
// Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
// TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
// back to the direct resolve).
Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
VkExtent2D extent);
void DestroyMultisampleResolveScratchImage();
struct DeferredDepthMipmapCleanup {
@@ -881,14 +841,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height);
void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload);
void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
GLenum buffer, GLint drawbuffer,
const ClearAttachmentPayload& clearPayload,
const VkClearRect& clearRect);
// ---- Submission fence tracking (GL sync objects) ----
// One record per vkQueueSubmit still in flight, in ascending submit
// order. Present/readback submissions reference the frame slot's
@@ -1588,74 +1540,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ShutdownDepthMipmapResources();
void CollectDeferredDepthMipmapCleanup(Uint32 frameIndex);
void DestroyDeferredDepthMipmapCleanup();
Bool TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
MG_State::GLState::FramebufferObject& readFbo,
MG_State::GLState::FramebufferObject& drawFbo,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
// Clears one layer of a colour image through a throwaway render pass whose entire content
// is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
// Records the texture's queued clears. By default the texture is synced first, which
// uploads any pending texels BEFORE the clear - right only for texels written before it.
// `uploadPendingTexels = false` leaves them pending (OrderPendingUploadAfterRecording).
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
Bool uploadPendingTexels = true);
// Texture uploads run in a batch submitted ahead of the frame's recording, so the upload
// the next sync of `texture` records would land BEFORE a write already recorded into the
// same image (a render, clear, blit or copy), and before a clear still queued for it (such
// a clear predates the texels: SettleTexelsBeforeQueuedClear). When either holds, this
// records the queued clear without the texels and submits the recording, so the upload
// follows both. An image the recording only READ keeps the old trade: no submit, and its
// earlier reads see the newer texels. It may end the render pass and replace
// frame.commandBuffer (recording again on return), so call it only where the caller holds
// no render-pass or command-buffer state across it. Returns whether it submitted.
Bool OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture);
// Called for each texture a glClear/glClearBuffer* is about to queue a clear on: uploads
// whatever texels are still pending (and mints the image if there is none), so that any
// texel pending when the clear is materialized was written after it.
void SettleTexelsBeforeQueuedClear(const MG_State::GLState::FramebufferObject& framebuffer,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
// The default framebuffer's twin of the two above. It cannot go through
// MaterializePendingClearForTexture: the default FBO's colour attachment is a
// placeholder texture object, and syncing THAT would clear a texture image nobody
// presents instead of the acquired swapchain image.
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
MG_State::GLState::FramebufferObject& fbo,
FramebufferAttachmentType attachmentType);
// Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
// different clear command and per-aspect masking.
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
const ClearAttachmentPayload& payload);
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
public:
// Submits whatever is recorded and waits for it. The CPU is about to read memory
// a shader wrote (a mapped shader storage buffer), and coherent host-visible
// storage only guarantees visibility once the work that produced it has retired.
Bool FinishPendingGpuWork();
private:
void ShutdownSwapchain();
@@ -1191,39 +1191,6 @@ namespace {
};
} // namespace
TEST_F(FramebufferTest, WidenedDrawBufferIsIdentifiedPerDrawBufferSlotNotPerAttachmentPoint) {
ScopedBackendOverride backend(MakeUnique<ThreeChannelAttachmentBackend>(/*substituted=*/true));
// Complementary's `composite` framebuffer again: draw buffer 0 is a natively renderable
// RGBA8, draw buffer 1 is the widened RGB8_SNORM. Only the second may be doctored.
GLuint framebuffer = 0;
GLuint colortex7 = 0;
GLuint colortex1 = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex7);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &colortex1);
MG_Impl::GLImpl::TextureStorage2D(colortex7, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::TextureStorage2D(colortex1, 1, GL_RGB8_SNORM, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, colortex7, 0);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, colortex1, 0);
auto& framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(framebufferObject, nullptr);
framebufferObject->SetDrawBuffer(0, FramebufferAttachmentType::Color0);
framebufferObject->SetDrawBuffer(1, FramebufferAttachmentType::Color1);
EXPECT_EQ(MG_Backend::DirectGLES::FramebufferImpl::ComputeAlphaWidenedDrawBufferMask(*framebufferObject),
1u << 1);
// Swapping the draw-buffer array moves the bit with the SLOT, not with the attachment point:
// glColorMaski and glClearBufferfv both address slots.
framebufferObject->SetDrawBuffer(0, FramebufferAttachmentType::Color1);
framebufferObject->SetDrawBuffer(1, FramebufferAttachmentType::Color0);
EXPECT_EQ(MG_Backend::DirectGLES::FramebufferImpl::ComputeAlphaWidenedDrawBufferMask(*framebufferObject),
1u << 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff) {
ScopedRenderStateDriverStubs driver;
MG_Backend::DirectGLES::FramebufferImpl::g_alphaWidenedDrawBufferMask = 1u << 1;
+13 -4
View File
@@ -30,6 +30,7 @@
// P5 b1: MGPipeResourceTrackerInstance(), so the split probe case can ask the PRODUCTION
// tracker for a buffer's handle instead of minting one by hand.
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
@@ -373,12 +374,20 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) {
TEST(DirectGLESSanity, ProvidesNamedFramebufferBlitForDirectStateAccess) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
// P13 W6: the backend applies a blit (BlitFramebuffer, which the record sink calls with the
// verb's framebuffer handles); the named-framebuffer DSA entries are the verb port's alone -
// they record a verb naming the framebuffer, and the backend never sees a frontend object.
const auto& funcs = backend.GetBackendFunctions().GL;
EXPECT_NE(funcs.ClearNamedFramebufferfv, nullptr);
EXPECT_NE(funcs.ClearNamedFramebufferfi, nullptr);
EXPECT_NE(funcs.BlitFramebuffer, nullptr);
EXPECT_NE(funcs.BlitNamedFramebuffer, nullptr);
EXPECT_EQ(funcs.ClearNamedFramebufferfv, nullptr);
EXPECT_EQ(funcs.BlitNamedFramebuffer, nullptr);
MobileGL::MG_Backend::GlobalBackendFunctionsTable table = backend.GetBackendFunctions();
MobileGL::MG_Record::InstallMonolithVerbPort(table);
EXPECT_NE(table.GL.ClearNamedFramebufferfv, nullptr);
EXPECT_NE(table.GL.ClearNamedFramebufferfi, nullptr);
EXPECT_NE(table.GL.BlitFramebuffer, nullptr);
EXPECT_NE(table.GL.BlitNamedFramebuffer, nullptr);
}
TEST(DirectGLESSanity, RewritesBaseInstanceBuiltinForEsslVertexShaders) {
-17
View File
@@ -37,20 +37,3 @@ target_link_libraries(
gtest_discover_tests(TextureViewTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
VkClearManagerTest
VkClearManagerTest.cpp
)
target_include_directories(VkClearManagerTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
VkClearManagerTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
gtest_discover_tests(VkClearManagerTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)