diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 5849b06f..66e41f18 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -399,18 +399,6 @@ namespace MobileGL::MG_Config { // Fatal{UnmigratedPipeInput} (negative control B). Unknown name is // Fatal{PipeVerifyBadKnob}. String PipePoisonOmit; - // MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the - // OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on - // whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the - // handle arm's {slot, gen} generation - so a replacement object inherits its dead - // predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts - // the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote - // this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap - // block is never handed back, so that spelling collided with nothing and the control - // went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the - // measurement and for what the control still leaves standing. Under - // MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build. - Bool PipeHandleAbaControl = false; // MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips, // texture pulls, upload shapes, residual-block bytes, index mirror bytes). Bool PipeStats = false; diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 3f259e6b..a5fea0ed 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -412,7 +412,6 @@ namespace MobileGL::MG_ConfigLoader { QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff; QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, ""); QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, ""); - features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL"); features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS"); features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096); features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b5873528..3b9a191d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2236,50 +2236,6 @@ namespace MobileGL::MG_Backend::DirectGLES { const SharedPtr* ElementBuffer = nullptr; }; - static Bool ReadClientSnapshotBytes(void* user, MG_Pipe::MGPipeClientBufferKind kind, Uint64 offset, SizeT size, - void* destination) { - if (kind != MG_Pipe::MGPipeClientBufferKind::Element) return false; - const auto& buffer = *static_cast(user)->ElementBuffer; - if (!buffer || offset > buffer->GetSize() || size > buffer->GetSize() - offset) return false; - // A shader-written index stream is exactly the case this snapshot exists for, so the - // object's shadow is reconciled before it is read (SyncGpuWrites is a no-op otherwise). - buffer->SyncGpuWrites(); - buffer->DownloadSubData(destination, static_cast(offset), size); - return true; - } - - // The draw's own fetch, spelled as the range record spells it: `indexSize` 0 is arrays, - // `elementStart` is the first element (an element offset into the bound element buffer, or - // 0 for the application's own index array), and `clientIndices` names that array when - // there is one. FALSE means the caller must SKIP the draw: the elements it would fetch are - // not in the store the driver is about to read, and issuing it anyway is a wrong picture - // rather than an error. - // Does the VAO feed any enabled attribute from client memory? `vertexRate`, when given, - // is set when one of those attributes advances per vertex rather than per instance. - static Bool VaoHasEnabledClientArray(const MG_State::GLState::VertexArrayObject& vao, - Bool* vertexRate = nullptr) { - Bool any = false; - const auto& attributes = vao.GetAllAttributes(); - for (SizeT i = 0; i < attributes.size(); ++i) { - if (!attributes[i].Enabled || attributes[i].Buffer) continue; - any = true; - if (vertexRate != nullptr) *vertexRate |= attributes[i].Divisor == 0; - } - return any; - } - - static Bool SyncClientSideVertexArraysForFetch(Uint8 indexSize, Uint32 elementStart, GLsizei count, - GLsizei instanceCount, GLint baseVertex, Uint32 baseInstance, - const void* clientIndices, Uint64 clientIndexBytes) { - if (count <= 0 || instanceCount <= 0) return true; - // MONOLITH ONLY, and the gate is what keeps a live transport's apply thread out of - // the frontend VAO: with a transport the CLIENT owns those bytes and snapshots them - // from the GL thread (MG_Impl/Pipe/OwnedDrawInputs.h), so there is nothing for the - // server to stage - and reading gPipeInputs there is the role violation this family's - // other monolith glue is guarded against. - return true; - } - // The indirect executors' form: the command's own words ARE the draw's fetch, and a // command block a shader wrote this frame is exactly what these shapes exist for - so the // command buffer's shadow is reconciled before it is read, and `commandBytes` (the @@ -10623,66 +10579,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - static Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize) { - Int maxDimension = std::max( - baseTexelSize.x(), - std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); - Uint mipLevelCount = 1; - while (maxDimension > 1) { - maxDimension = std::max(maxDimension / 2, 1); - ++mipLevelCount; - } - return mipLevelCount; - } - - static IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel) { - return { - std::max(baseTexelSize.x() >> static_cast(relativeLevel), 1), - std::max(baseTexelSize.y() >> static_cast(relativeLevel), 1), - std::max(baseTexelSize.z() >> static_cast(relativeLevel), 1), - }; - } - - static Bool EnsureGenerateMipmapStorageAllocated(MG_State::GLState::TextureObjectMipmap& texture, - TextureUploadTarget uploadTarget, Bool& allocatedStorage) { - const Uint existingLevelCount = texture.GetMipmapLevelCount(); - if (existingLevelCount == 0) { - return false; - } - // P4a (D-M). glGenerateMipmap's storage grow reaches into the frontend texture's own - // level shadows to decide - and then to define - the levels the driver is about to - // fill. In monolith that is exactly what it does today and nothing here changes; under - // a split there is no client address space to reach into, so P8 gives this name teeth. - // Named and greppable rather than silent, so the site cannot quietly disappear before - // then. - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); - - const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, 0); - const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, 0); - const SizeT baseTexelCount = static_cast(baseTexelSize.x()) * - static_cast(baseTexelSize.y()) * - static_cast(baseTexelSize.z()); - if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 || - baseByteSize == 0 || baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) { - return false; - } - - const SizeT bytesPerTexel = baseByteSize / baseTexelCount; - const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize); - if (existingLevelCount < requiredLevelCount) { - allocatedStorage = true; - } - for (Uint level = existingLevelCount; level < requiredLevelCount; ++level) { - const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level); - const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * - static_cast(levelTexelSize.y()) * - static_cast(levelTexelSize.z()); - texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); - texture.MarkStorageDirty(uploadTarget, level, false); - } - return true; - } - // P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two // descriptor reads this file makes (Managers.cpp has the same inverse beside the storage // sync; it is file-local there, and a header for eleven cases would be the wrong trade). @@ -10740,21 +10636,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return window; } - static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr& texture) { - // P8-B1: the transport arm that stood here (a descriptor check under the same - // `generate-mipmap-storage` name) was unreachable - GenerateMipmap returns through - // GenerateMipmapByRecord before this call on every split arm - and the record arm's - // window above replaces the check it made. - auto* mipmapTexture = dynamic_cast(texture.get()); - MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage."); - Bool allocatedStorage = false; - for (const TextureUploadTarget uploadTarget : texture->GetUploadTargets()) { - const Bool allocated = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, uploadTarget, allocatedStorage); - MOBILEGL_ASSERT(allocated, "GenerateMipmap could not allocate generated mipmap storage."); - } - return allocatedStorage; - } - static void AssertNoGLError(const char* operation) { const GLenum err = g_GLESFuncs.glGetError(); MOBILEGL_ASSERT(err == GL_NO_ERROR, "%s failed: %s", operation, @@ -11444,137 +11325,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - static void GenerateDepthTexture2DMipmap( - const SharedPtr& texture, - const SharedPtr& backendTexture) { - MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateDepthTexture2DMipmap needs texture."); - MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D, - "DirectGLES depth mipmap generation only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(IsDepthOnlyFormat(texture->GetFormat()), - "DirectGLES depth mipmap generation requires a depth-only texture."); - - auto* mipmapTexture = dynamic_cast(texture.get()); - MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage."); - - // P5e (tx2): the transport arm that stood here - a scoped `HandleOf(texture.get())` - // probe to re-derive a handle the generate_mipmap record already carried (CONTRACT-P5E - // §4.4's site 9161) - is GenerateDepthTexture2DMipmapByRecord above, reached from - // GenerateMipmapByRecord with the handle passed in. This overload is the monolith body. - - const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount(); - MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage."); - - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint level = 1; level < mipLevelCount; ++level) { - const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1); - const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level); - BlitDepthTexture2D(textureId, static_cast(level - 1), 0, 0, - static_cast(srcSize.x()), static_cast(srcSize.y()), - textureId, static_cast(level), 0, 0, - static_cast(dstSize.x()), static_cast(dstSize.y())); - } - } - - static void GenerateColorTexture2DMipmap( - const SharedPtr& texture, - const SharedPtr& backendTexture) { - MOBILEGL_ASSERT(texture != nullptr && backendTexture != nullptr, "GenerateColorTexture2DMipmap needs texture."); - MOBILEGL_ASSERT(texture->GetTarget() == TextureTarget::Texture2D, - "DirectGLES color mipmap generation only supports GL_TEXTURE_2D."); - MOBILEGL_ASSERT(IsColorOnlyFormat(texture->GetFormat()), - "DirectGLES color mipmap generation requires a color-only texture."); - - auto* mipmapTexture = dynamic_cast(texture.get()); - MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage."); - - // P5e (tx2): the transport arm that stood here is GenerateColorTexture2DMipmapByRecord - // above (CONTRACT-P5E §4.4's site 9216); this overload is the monolith body. - - const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount(); - MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage."); - - const GLenum filter = IsIntegerColorFormat(texture->GetFormat()) ? GL_NEAREST : GL_LINEAR; - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint level = 1; level < mipLevelCount; ++level) { - const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level - 1); - const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(TextureUploadTarget::Texture2D, level); - BlitColorTexture2D(textureId, static_cast(level - 1), 0, 0, - static_cast(srcSize.x()), static_cast(srcSize.y()), - textureId, static_cast(level), 0, 0, - static_cast(dstSize.x()), static_cast(dstSize.y()), filter); - } - } - - // Where a target keeps its LAYER count, the state-side twin of the wire descriptor's - // ArrayLayers: a 1D array's layers live in the state-side height, every other layered target - // keeps them in z, a cube map has six faces, and everything else has one. - static Uint TextureLayerCount(const SharedPtr& texture) { - const IntVec3 base = texture->GetBaseSize(); - switch (texture->GetTarget()) { - case TextureTarget::Texture1DArray: - return static_cast(std::max(base.y(), 1)); - case TextureTarget::Texture2DArray: - case TextureTarget::TextureCubeMapArray: - case TextureTarget::Texture2DMultisampleArray: - return static_cast(std::max(base.z(), 1)); - case TextureTarget::TextureCubeMap: - return 6; - default: - return 1; - } - } - - // The monolith twin of the record arm's view-window rule. Same window, read off the frontend - // objects this arm still has: the storage owner is what the view's texels belong to, and the - // view's own level range and layer window are stated relative to it. - static Bool GenerateMipmapThroughViewWindowForTexture(const SharedPtr& texture) { - MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmapThroughViewWindowForTexture needs a texture."); - if (!texture->IsTextureView()) return false; - const auto& owner = texture->GetViewStorageOwner(); - if (owner == nullptr) return false; - if (texture->GetSamples() != 0) return false; - const Bool depth = IsDepthOnlyFormat(texture->GetFormat()); - if (!depth && !IsColorOnlyFormat(texture->GetFormat())) return false; - auto* mipmapTexture = dynamic_cast(texture.get()); - if (mipmapTexture == nullptr || texture->GetUploadTargets().empty()) return false; - - const Uint minLayer = texture->GetViewMinLayer(); - const Uint numLayers = texture->GetViewNumLayers(); - if (numLayers == 0) return false; - // A window that covers the whole storage is what the native call already does. - if (minLayer == 0 && numLayers >= TextureLayerCount(owner)) return false; - - MipmapViewWindow window; - window.MinLevel = texture->GetViewMinLevel(); - window.MinLayer = minLayer; - window.NumLayers = numLayers; - const auto levelRange = texture->GetLevelRange(); - // The view's chain ends at the first of: its MAX_LEVEL, its window's level count, and - // the levels its storage actually has. - window.LogicalBase = levelRange.x(); - window.LogicalEnd = std::min({static_cast(levelRange.y()) + 1, - static_cast(texture->GetViewNumLevels()), - static_cast(mipmapTexture->GetMipmapLevelCount())}); - window.Depth = depth; - window.Filter = IsIntegerColorFormat(texture->GetFormat()) ? GL_NEAREST : GL_LINEAR; - if (window.LogicalEnd <= window.LogicalBase + 1) return true; - - // The STORAGE's ES name is what the blits write: the view's own name aliases the same - // texels, but only the owner's name carries every level and layer this window reaches. - auto& storage = TextureImpl::SyncTextureObjectToBackend(owner); - if (!storage || storage->GetBackendTextureId() == 0) { - MGLOG_E_ONCE("DirectGLES: generate_mipmap through texture view %u has no storage texture to write; " - "the call is dropped", texture->GetExternalIndex()); - return true; - } - window.StorageTextureId = storage->GetBackendTextureId(); - const TextureUploadTarget uploadTarget = texture->GetUploadTargets()[0]; - GenerateMipmapThroughViewWindow(window, [mipmapTexture, uploadTarget](Uint32 level) { - return mipmapTexture->GetMipmapTexelSize(uploadTarget, level); - }); - return true; - } - void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG @@ -11770,84 +11520,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - // ES has no colour-renderable three-channel float format, and its glGenerateMipmap - // requires one, so it rejects GL_RGB16F and GL_RGB32F outright where every desktop - // driver accepts them. Both store a plain array of floats, and a format the driver - // cannot render into is a format nothing can have rendered into - so the frontend's own - // copy of the texels is the authority, and the chain can be filtered there and carried - // down by the ordinary upload path. Returns false for anything else, leaving the - // driver's answer (including its error) in place. - static Bool GenerateThreeChannelFloatMipmapOnCpu( - const SharedPtr& texture) { - if (!texture) return false; - const TextureInternalFormat format = texture->GetFormat(); - const Bool isHalf = format == TextureInternalFormat::RGB16F; - if (!isHalf && format != TextureInternalFormat::RGB32F) return false; - - auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get()); - if (mipmapTexture == nullptr) return false; - // P4a (D-M). The three-channel float mipmap fallback filters the CHAIN ON THE CPU out - // of the frontend's level shadows and uploads the result. Monolith keeps doing exactly - // that; a split server has no shadow to filter, and P8 is what retires it. The texels - // themselves are the other half of ARCHITECTURE.md's generate_mipmap item, which is - // P8's too. - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-fallback"); - const Uint levelCount = mipmapTexture->GetMipmapLevelCount(); - constexpr Int kChannels = 3; - - for (const auto uploadTarget : texture->GetUploadTargets()) { - for (Uint level = 1; level < levelCount; ++level) { - const IntVec3 srcSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level - 1); - const IntVec3 dstSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, level); - if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return false; - auto* src = static_cast(mipmapTexture->MapMipmapData(uploadTarget, level - 1)); - auto* dst = static_cast(mipmapTexture->MapMipmapData(uploadTarget, level)); - if (src == nullptr || dst == nullptr) return false; - - const SizeT componentBytes = isHalf ? sizeof(Uint16) : sizeof(Float); - const SizeT texelBytes = componentBytes * kChannels; - const auto load = [&](const Uint8* base, Int x, Int y, Int channel) { - const Uint8* texel = base + (static_cast(y) * srcSize.x() + x) * texelBytes + - channel * componentBytes; - if (isHalf) { - Uint16 bits = 0; - Memcpy(&bits, texel, sizeof(bits)); - return MG_Util::DecodeHalfBitsToFloat(bits); - } - Float value = 0.0f; - Memcpy(&value, texel, sizeof(value)); - return value; - }; - - // Box filter over the 2x2 source footprint, clamped where a dimension is - // already 1 (GL 4.6 core 8.14.4 leaves the exact filter to the implementation - // and this is the one it describes for power-of-two levels). - for (Int y = 0; y < dstSize.y(); ++y) { - for (Int x = 0; x < dstSize.x(); ++x) { - const Int x0 = std::min(x * 2, srcSize.x() - 1); - const Int x1 = std::min(x * 2 + 1, srcSize.x() - 1); - const Int y0 = std::min(y * 2, srcSize.y() - 1); - const Int y1 = std::min(y * 2 + 1, srcSize.y() - 1); - for (Int channel = 0; channel < kChannels; ++channel) { - const Float average = 0.25f * (load(src, x0, y0, channel) + load(src, x1, y0, channel) + - load(src, x0, y1, channel) + load(src, x1, y1, channel)); - Uint8* texel = dst + (static_cast(y) * dstSize.x() + x) * texelBytes + - channel * componentBytes; - if (isHalf) { - const Uint16 bits = MG_Util::EncodeFloatToHalfBits(average); - Memcpy(texel, &bits, sizeof(bits)); - } else { - Memcpy(texel, &average, sizeof(average)); - } - } - } - } - mipmapTexture->MarkStorageDirty(uploadTarget, level, true); - } - } - return true; - } - // P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL. // // ES generates a chain only for a colour-renderable, filterable format, and whether the @@ -12233,30 +11905,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } }; - // The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same - // find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject), - // reachable from a path that has a renderbuffer but no framebuffer. The MONOLITH arm's only: - // under a transport MakeGLESCopyImageEndpoint resolves a renderbuffer endpoint by handle and - // never calls this. (P8-E removed the `CopyImageSubData+RENDERBUFFER` latch that stood here - - // its transport condition could not hold at the only call site - and - // CopyImageStoreReadbackScenario's renderbuffer case runs the handle arm on every transport.) - static SharedPtr SyncRenderbufferObjectToBackend( - const SharedPtr& renderbufferObject) { - if (!renderbufferObject) return nullptr; - SharedPtr backendRenderbufferObject; - if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) { - backendRenderbufferObject = *slot; - } else { - auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject); - if (!newSlot) { - newSlot = MakeShared(); - } - backendRenderbufferObject = newSlot; - } - backendRenderbufferObject->SyncToBackend(renderbufferObject); - return backendRenderbufferObject; - } - static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y, GLint z, GLESCopyImageEndpoint& out) { if (endpoint.IsRenderbuffer()) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 460a1de3..097c41a1 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -10860,21 +10860,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } - // The colour attachment glReadPixels/glGetTexImage would read from, or nullptr when the - // read buffer names no colour attachment at all. - static const MG_State::GLState::FramebufferAttachmentObject* GetReadColorAttachment() { - const auto& readFBO = - MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); - if (!readFBO) { - return nullptr; - } - const auto readBuffer = readFBO->GetReadBuffer(); - if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) { - return nullptr; - } - return &readFBO->GetAttachment(readBuffer); - } - Bool IsAlphaWidenedColorAttachment( const MG_State::GLState::FramebufferAttachmentObject& attachmentObject) { if (attachmentObject.IsTexture()) { @@ -15798,14 +15783,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // A package REPLACES the body in place and deletes the matching comment; it does not add a // second definition elsewhere, or the linker's answer depends on link order. namespace { - [[noreturn]] void MGPipeP5eSeamNotLanded(const char* name, const char* owner, - MG_Pipe::MGPipeHandle handle) { - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"} - the P5e by-handle seam is declared by " - "package c0e and bodied by package %s; it was called for handle {%u, %u} " - "before that package landed", - name, owner, handle.Slot, handle.Gen); - std::abort(); - } } // namespace namespace TextureImpl { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 9cc0307b..e10741f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -32,11 +32,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return MG_Config::Features.MagmaR11G11B10FFallback; } - Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { - (void)dpy; - return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT; - } - Bool IsFormatIndexValid(TextureInternalFormat format) { return format != TextureInternalFormat::Unknown && static_cast(format) >= 0 && static_cast(format) < kFormatCapabilityFormatCount; diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 36a19e1c..20a3ba34 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -38,10 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { RendererSlot pVulkanRenderer; namespace { - void RejectWireLegacyBuffer() { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); - std::abort(); - } // Generation of the live VulkanRenderer instance, mirroring // DirectGLES's g_syncContextGeneration. BackendObject_DirectVulkan // bumps it (BumpRendererGeneration) wherever pVulkanRenderer is reset @@ -149,19 +145,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; - void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { - if (!pixels || width <= 0 || height <= 0) { - return; - } - const auto inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format); - const auto inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); - const SizeT size = MG_Util::CalculateInputTextureImageSize(inputFormat, inputType, - IntVec3(width, height, 1)); - if (size > 0) { - std::memset(pixels, 0, size); - } - } - // The verb's handles identify server stores; never inspect a client binding. // @@ -243,41 +226,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) { - if (!MG_Pipe::gPipeInputs.ValidateProgramName(program)) { - return nullptr; - } - auto& programObject = MG_Pipe::gPipeInputs.GetProgramObject(program); - return programObject.get(); - } - - const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { - auto drawBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - if (drawBuffer) { - drawBuffer->SyncPersistentMappedRange(); - // A command block a compute shader wrote is the case this whole shape exists - // for, and every caller here reads the words on the CPU - the per-command - // expansion, the baseInstance the shader emulation is fed, and the fetch - // range a client-memory vertex array's upload is bounded by. Reconciling the - // shadow first is what keeps those readings the real ones (a no-op unless a - // GPU write is pending). - drawBuffer->SyncGpuWrites(); - const SizeT commandOffset = reinterpret_cast(indirect); - if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) { - MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); - return nullptr; - } - return drawBuffer->MappedData() + commandOffset; - } - - if (!indirect) { - MGLOG_E_ONCE("%s skipped: indirect pointer is null", label); - return nullptr; - } - - return reinterpret_cast(indirect); - } - } // namespace // P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): THE MONOLITH CONSUMER READS THE ARCHIVE diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h index 86121d18..669f4658 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h @@ -18,8 +18,9 @@ #include -// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the -// {slot, gen} mint the re-keyed sites are written against. +// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). (The {slot, gen} +// mint that used to live here keyed the frontend arm's VAO memos and went with that arm in P13 +// W6; the record arm keys every object on the applier's own handles.) // // Two switches decide which arm a re-keyed site runs, and they are NOT the same switch: // @@ -102,492 +103,4 @@ namespace MobileGL::MG_Backend::DirectVulkan { static_cast(MG_Pipe::kMGPipeSubsystemsMigratedAtP2)); std::abort(); } - - // "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H - // site asks, so that they cannot disagree with each other or with the startup gate. - inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) { - // No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration - // has already made a clear bit a startup Fatal, so the handle arm is the only arm a - // running process can be on. - (void)trackHBit; - return true; - } - - // --------------------------------------------------------------------------------- - // Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL - // --------------------------------------------------------------------------------- - // - // "Is the object-identity half of every vertex-input memo key deliberately defeated in - // this run?" - the ONE question the control's sites ask, for the same reason - // MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree, - // and a control that defeats two of three guards proves nothing. - // - // WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT" - // RATHER THAN "USE THE HEAP ADDRESS". - // - // D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip - // the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at - // the freed heap block and so reproduces the key. Measured, it does not: in - // HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight - // out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for - // glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next - // allocation the replacement path makes. Four create/delete cycles in one run produced - // four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for - // "hash the address" to collide with: the replacement hashes differently, indexes a - // different memo slot, and inherits nothing - so the arm asserted stale pixels and saw - // fresh ones, which is a FAILING negative control that had stopped controlling anything. - // - // So the control no longer asks the allocator for the collision; it manufactures it. On - // both arms the object identity is replaced by a constant, which is the strongest form of - // "the allocator handed the block back" and is deterministic. That covers strictly more - // than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm - // the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm - // ships - so the replacement VAO is handed the dead one's memo entry and its content hash. - // Defeating only the retired lifetime-id/address guards would leave that key untested, - // which is exactly the vacuity this control exists to catch. - // - // WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1, - // MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}: - // - // * THIS MINT still has no death notification, and that is now a narrower statement than - // it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table - // (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so - // NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's - // one job is to emit the `object_death` RECORD so the server drops its twin, and it - // never touches these per-renderer identity tables: they are the SERVER-side renderer's - // own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong - // side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still - // returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval - // 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive - // from that is unchanged; - // * HandleRecycleScenario issues five frame boundaries, so the free list is empty when - // the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured: - // redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is - // decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green; - // * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last - // draw, which necessarily puts the two draws in different frames - and the only memo - // that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines - // across frames by design. The two requirements are mutually exclusive, so the - // generation is out of reach of any same-frame pixel reproducer for this memo. - // - // The generation is covered where it IS expressible, over this mint and the claim rule - // MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real - // retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at - // {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that - // suite; it is the only place in the tree where that deletion is caught. - // - // Everything the control does NOT defeat is as load-bearing as what it does. It never - // touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame - // serial, its slice-epoch compares and its host-map check all stay in force, so a green - // AbaControl arm still means "a replacement object was handed its dead predecessor's - // resolved vertex bindings because the identity halves of the keys were defeated", not - // "every safety net was switched off until something broke". - // - // Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and - // #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it. - // P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them". - // - // P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso, - // SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half - // of P4a's memo keys as well", because a control that only defeats the guards a phase - // RETIRED says nothing about the key that phase SHIPS. - // - // On Magma there is no such key to defeat, and that is a fact about the roadmap rather than - // an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds, - // VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program - // are all still reached from their frontend objects on this backend, and moving them onto - // handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan - // untouched apart from this file). So the honest statement is per KIND, and it is spelled as - // code rather than as a comment so that a caller cannot read the blanket answer above and - // conclude the knob covers its kind: - // - // * for the two kinds this backend really keys on {slot, gen}, the knob defeats the - // identity exactly as it always has (MagmaPipeClaimSlotMemos); - // * for P4a's six there is nothing here to defeat, so the answer is FALSE - and - // MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and - // makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is - // not controlling anything for that kind. It does not assert a corruption that no code - // on this tree can produce, which would be a permanently red always-on lane. - // - // WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it: - // when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot - // tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already - // has for vertex input - this function's per-kind answer becomes that consumer's, the - // integration probe finds the consumer, and the six cases flip to expecting the corruption. - inline Bool MagmaPipeAbaControlDefeatsIdentity() { - return MG_Config::Features.PipeHandleAbaControl; - } - - // WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob - // above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind. - // - // EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason: - // a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this - // file rather than a row that silently inherits somebody else's answer. Being wrong in the - // "covered" direction is the expensive one - a control asserting a corruption nobody can - // produce is a permanently red always-on lane - so an undecided kind must never read true, - // and with no `default:` there is no arm for it to read true from. - // - // constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a - // predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the - // asserts say so in both directions, and the file no longer compiles if the tables and this - // statement of them ever part company. (Review F-m5: the earlier form had no caller at all - // and could not make anything red or green.) - inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) { - switch (kind) { - // The two MagmaPipeIdentityTables really mints. - case MG_Pipe::MGPipeKind::VertexElementsCso: - case MG_Pipe::MGPipeKind::Buffer: - return true; - // P4a's six object classes: still reached from their frontend objects on this - // backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for - // the knob to defeat. - case MG_Pipe::MGPipeKind::Texture: - case MG_Pipe::MGPipeKind::Renderbuffer: - case MG_Pipe::MGPipeKind::Framebuffer: - case MG_Pipe::MGPipeKind::SamplerCso: - case MG_Pipe::MGPipeKind::SamplerViewCso: - case MG_Pipe::MGPipeKind::ShaderCso: - // ...and everything else this backend does not mint a handle for. - case MG_Pipe::MGPipeKind::None: - case MG_Pipe::MGPipeKind::Xfb: - case MG_Pipe::MGPipeKind::RenderStateCso: - case MG_Pipe::MGPipeKind::Fence: - case MG_Pipe::MGPipeKind::Query: - case MG_Pipe::MGPipeKind::Context: - case MG_Pipe::MGPipeKind::KindCount: - return false; - } - return false; - } - - static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso), - "MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to " - "defeat for it"); - static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer), - "MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it"); - static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) && - !MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) && - !MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) && - !MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) && - !MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) && - !MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso), - "P4a's six object classes are not keyed on {slot, gen} on this backend, so " - "HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. " - "Wiring one of them is what flips this assert, this predicate and that arm - and " - "MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what " - "carries the answer into the lane"); - - // THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier - // round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two - // statements above - and it had no caller anywhere in the tree: the knob's only two - // consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline - // key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a - // variable at either site. A conjunction no build ever evaluates cannot be pinned the way - // the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features, - // so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about, - // one level up: an `&&` whose operands could be inverted or dropped with nothing to say so. - // - // The two pieces stand alone instead, and each is pinned by something that runs: - // MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the - // three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats - // Identity is the knob, and its two consumers are what make it true or false. A call site - // that ever does hold a variable kind writes the `&&` there, where a build will run it. - - // The single consumer-table entry every VAO collapses onto while the control is on. Slot - // 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable - // handle onto it), so nothing about the tables changes shape for the control's sake. - inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0; - - // --------------------------------------------------------------------------------- - // The {slot, gen} mint - // --------------------------------------------------------------------------------- - // - // Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three - // properties, and the third is the one review v2 got wrong: - // - // 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can - // never match a live object even if the allocator hands back the same heap address - // (the ABA HandleRecycleScenario reproduces); - // 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash, - // no probe and no mix; - // 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being - // drawn, whatever the working set size. - // - // Property 3 is why this is not the fixed 2-way set-associative LRU the previous round - // shipped. That structure evicted a LIVE object once the working set passed its capacity, - // and every consumer memo keyed on the handle died with it: measured on a verbatim - // transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and - // 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys - // VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it - // fed - the content-hash memo and the resolved-state memo - had NO capacity before this - // package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction - // there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the - // buffer table thrashes too, makes the vertex-input content hash a per-draw value that - // inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every - // draw. That is a worse leak than the one it was introduced to avoid. - // - // So: grow on demand, and reclaim by AGE instead of by capacity. - // - // * Acquire hits an UnorderedMap, in front of which sits a - // one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo - // turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64 - // compares - less than the address multiply plus two-way probe the pre-handle arm ran. - // * OnFrameBoundary retires slots whose object has not been drawn for - // kRetireAgeBoundaries boundaries and returns them to a free list, so the table's - // footprint tracks the LIVE DRAWN working set, not objects ever created. That is the - // property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its - // Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed - // a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no - // death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's - // six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState - // plus one map node per object EVER created, for the life of the process, on a - // platform with an LMK. Age-based reclamation is the stand-in for the death - // notification, and it is exactly as ABA-proof, because reuse bumps Gen. - // * A retire costs at most one memo recompute if the object is drawn again - the same - // price a cache miss costs - and it is charged only to objects that went idle for - // ~1024 frames, never to a hot one. - // - // Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against - // the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is - // no capacity to size off a device measurement because there is no capacity; what the - // device run in D.4.2 can still want is the number itself, so the high-water mark is - // logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a - // draw - ROADMAP.md:7). - // - // Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see - // MagmaPipeIdentityTables): a process-global would share one table, and one reclamation - // clock, across two live contexts. - class MagmaPipeIdentityTable { - public: - explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {} - - // Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many - // entries; MagmaPipeSlotTable below grows itself, so nobody has to ask. - Uint32 Count() const { return static_cast(m_entries.size()); } - // Objects currently holding a slot - the live working set this table tracks. - Uint32 LiveCount() const { return static_cast(m_index.size()); } - - MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) { - // Unreachable: MG_State hands out lifetime ids from 1 precisely so that a - // zero-initialised memo slot cannot carry a live object's id. Guarded anyway so - // that a zero can never be minted into a slot and then indexed with. - if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle; - // The one-entry front memo. Cleared by any retire, so it can never serve a slot - // that has been handed back to the free list. - if (lifetimeId == m_lastLifetimeId) { - m_entries[m_lastIndex].LastUse = m_boundary; - return m_lastHandle; - } - Uint32 index = 0; - const auto it = m_index.find(lifetimeId); - if (it != m_index.end()) { - index = it->second; - } else { - index = ClaimSlot(); - m_entries[index].LifetimeId = lifetimeId; - m_index.emplace(lifetimeId, index); - } - Entry& entry = m_entries[index]; - entry.LastUse = m_boundary; - m_lastLifetimeId = lifetimeId; - m_lastIndex = index; - m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot, - entry.Gen}; - return m_lastHandle; - } - - // Ages the table and returns idle slots to the free list. Same shape and the same - // self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed - // slots' consumers use. - void OnFrameBoundary() { - ++m_boundary; - if ((m_boundary % kSweepInterval) != 0) return; - SizeT retired = 0; - for (auto it = m_index.begin(); it != m_index.end();) { - Entry& entry = m_entries[it->second]; - if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) { - entry.LifetimeId = 0; - m_freeSlots.push_back(it->second); - it = m_index.erase(it); - ++retired; - } else { - ++it; - } - } - if (retired != 0) { - // A retired slot's Gen has not moved yet - it moves when the slot is reused - - // so a front memo pointing at one would still hand out a handle the consumer - // tables would accept. Drop it. - m_lastLifetimeId = 0; - m_lastHandle = MG_Pipe::kMGPipeNullHandle; - MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted", - m_kindName, retired, LiveCount(), Count()); - } - } - - private: - // Sweep cadence and retirement age, deliberately the same numbers - // VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot - // retired earlier than its cache entry would mint a new handle for an object whose - // entry is still live and still correct, which is a pure waste. - static constexpr Uint64 kSweepInterval = 256; - static constexpr Uint64 kRetireAgeBoundaries = 1024; - - struct Entry { - Uint64 LifetimeId = 0; - Uint64 LastUse = 0; - // Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps - // its generation, which is what makes a memo survive a reconfiguration. - Uint32 Gen = 0; - }; - - Uint32 ClaimSlot() { - while (!m_freeSlots.empty()) { - const Uint32 index = m_freeSlots.back(); - m_freeSlots.pop_back(); - // MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and - // MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are - // live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the - // RELEASE path instead of asserted: a slot that has been reused 2^32 times is - // permanently retired rather than wrapped, because a wrapped Gen would let a - // stale handle match a live object. It costs one slot. - if (m_entries[index].Gen == ~Uint32{0}) { - MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is " - "retired for good; {slot, gen} stays unique", - m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot); - continue; - } - ++m_entries[index].Gen; - return index; - } - const Uint32 index = static_cast(m_entries.size()); - m_entries.push_back(Entry{}); - m_entries[index].Gen = 1; - // The high-water mark, at powers of two from 1024 up: at most a handful of lines - // for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per - // object this backend has ever seen and never on a draw (ROADMAP.md:7). - // - // [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This - // one is I, not D, because D is compiled out of every build that ships and of every - // build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for: - // the live-object high-water mark of minecraft-1.21.4-in-world and - // ...-sodium-in-world, which nothing on desktop reaches and no gate here can see. - // The structure no longer has a capacity to size off it, so the number is evidence - // rather than a tuning input - but D.4.2 should still read it out of the device log, - // and it cannot read a line that was compiled away. - const SizeT minted = m_entries.size(); - if (minted >= 1024 && (minted & (minted - 1)) == 0) { - MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live", - m_kindName, minted, LiveCount()); - } - return index; - } - - const char* m_kindName = ""; - Uint64 m_boundary = 0; - Vector m_entries; - Vector m_freeSlots; - UnorderedMap m_index; - // One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live - // object's lifetime id is never 0. - Uint64 m_lastLifetimeId = 0; - Uint32 m_lastIndex = 0; - MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle; - }; - - // The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or - // a context recreation, which destroys and rebuilds the renderer) would otherwise share one - // table and one reclamation clock, and both consumer tables are per-instance already. - class MagmaPipeIdentityTables { - public: - // A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array - // resolves to, and the only kind in MGPipeKind that names vertex-input state. - MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { - return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId) - : m_vaos.Acquire(lifetimeId); - } - void OnFrameBoundary() { - m_vaos.OnFrameBoundary(); - m_buffers.OnFrameBoundary(); - } - const MagmaPipeIdentityTable& Vaos() const { return m_vaos; } - const MagmaPipeIdentityTable& Buffers() const { return m_buffers; } - - private: - MagmaPipeIdentityTable m_vaos{"VertexElementsCso"}; - MagmaPipeIdentityTable m_buffers{"Buffer"}; - }; - - // The table entry a handle names. Every per-slot table Magma keeps is indexed by this. - // - // A null handle has no slot, and it is unreachable here: both lifetime-id sources start at - // 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The - // ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles - // MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null - // handle can never match, because MGPipeHandleIsNull is also what the consumers compare. - inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) { - MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), - "a null MGPipeHandle has no slot to index a per-slot table with"); - return MG_Pipe::MGPipeHandleIsNull(handle) - ? 0u - : handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot; - } - - // A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE. - // - // D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a - // consumer needs - but a Vector that grows relocates its elements, and the draw path holds - // references into these entries across nested calls. Chunks of kChunkEntries are appended - // instead: the Vector of owning pointers reallocates, the chunks never do, so an entry - // reference is valid for the life of the table. That is the same guarantee the fixed table - // it replaces gave, without the fixed capacity. - template - class MagmaPipeSlotTable { - public: - T& operator[](Uint32 index) { - const Uint32 chunk = index / kChunkEntries; - while (m_chunks.size() <= chunk) { - m_chunks.push_back(MakeUnique()); - } - return m_chunks[chunk]->Entries[index % kChunkEntries]; - } - SizeT Capacity() const { return m_chunks.size() * kChunkEntries; } - - private: - struct Chunk { - T Entries[kChunkEntries] = {}; - }; - Vector> m_chunks; - }; - - // The claim rule every per-slot memo table uses, in one place so that the rule and the - // negative control that defeats it cannot drift apart between consumers - and so that the - // unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests - // this code rather than a copy of it. - // - // The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is - // this object's. A slot the mint recycled for a different object comes back with a moved - // Gen, so the compare fails and the entry is cleared rather than inherited. That is the - // half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity). - // - // With negative control C on, every object collapses onto one entry and the entry is handed - // back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's - // slot" and "the slot was reused and Gen did not move". - // - // `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that - // means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance. - template - inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable& table, - const MG_Pipe::MGPipeHandle& handle) { - if (MagmaPipeAbaControlDefeatsIdentity()) { - return table[kMagmaPipeAbaControlSlotIndex]; - } - Memos& memos = table[MagmaPipeSlotIndex(handle)]; - if (!(memos.Owner == handle)) { - memos = Memos{}; - memos.Owner = handle; - } - return memos; - } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h index 06c575fe..79d94ac3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaProgramSource.h @@ -19,36 +19,31 @@ namespace MobileGL::MG_Backend::DirectVulkan { class MagmaProgramSource { public: using Program = MG_State::GLState::ProgramObject; - MagmaProgramSource(const Program& program) : m_frontend(&program) {} MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record) : m_handle(handle), m_record(&record) { MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive"); } - Bool IsWire() const { return m_record != nullptr; } MG_Pipe::MGPipeHandle Handle() const { return m_handle; } - const Program* Frontend() const { return m_frontend; } - Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); } + Uint GetExternalIndex() const { return m_handle.Slot; } Uint64 GetLifetimeId() const { - return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId(); + return (Uint64(m_handle.Gen) << 32) | m_handle.Slot; } - Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); } - Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); } - Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); } + Bool GetLinkStatus() const { return m_record->Desc.LinkStatus != 0; } + Bool GetSpirvStatus() const { return m_record->Desc.SpirvStatus != 0; } + Bool PointSizeDemoted() const { return m_record->Desc.PointSizeDemoted != 0; } Bool GetSpirvValidationEnabled() const { - return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled(); + return m_record->Desc.EnableSpirvValidation != 0; } const Vector>& GetGeneratedSpirv() const { - return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv(); + return m_record->Archive->Spirv.generatedSpirv; } Vector GetLinkedShaderStages() const { - if (!IsWire()) return m_frontend->GetLinkedShaderStages(); Vector result; for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast(stage)); return result; } Bool HasLinkedShaderStage(ShaderStage stage) const { - if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage); const auto& stages = m_record->Archive->LinkedStages; return std::find(stages.begin(), stages.end(), static_cast(stage)) != stages.end(); } @@ -56,65 +51,58 @@ namespace MobileGL::MG_Backend::DirectVulkan { // A view has no lifetime in which a memo can safely persist. The server // factory's content cache still owns compiled programs; hash from current // archive + binding tails rather than borrowing the client's mutable memo. - return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash); + (void)flags; + (void)hash; + return false; } void SetBackendHashMemo(Uint flags, Uint64 hash) const { - if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash); } - Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); } - Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); } - Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); } - Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); } + Uint64 GetBackendStateVersion() const { return m_record->Serial; } + Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; } + Uint64 GetImageUnitVersion() const { return m_record->BindingsSerial; } + Uint64 GetLinkVersion() const { return m_record->Serial; } - const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); } - Uint GetUBOSize() const { return IsWire() ? static_cast(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); } - Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); } + const void* GetUBOData() const { return m_record->GlobalConstants.data(); } + Uint GetUBOSize() const { return static_cast(m_record->GlobalConstants.size()); } + Uint32 GetUBOContentVersion() const { return m_record->GlobalConstantsVersion; } Int GetActiveUniformBlocksCount() const { - return IsWire() ? static_cast(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount(); + return static_cast(Link().glBlockIndexToTProgram.size()); } Uint GetUniformBlockIndex(const char* name) const { - if (!IsWire()) return m_frontend->GetUniformBlockIndex(name); auto found = Link().uniformBlockIndexByName.find(name); if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]"); return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second; } Uint GetUniformBlockBinding(Uint index) const { - if (!IsWire()) return m_frontend->GetUniformBlockBinding(index); return index < m_record->BlockBindings.size() ? static_cast(m_record->BlockBindings[index]) : 0; } const String& GetUniformBlockName(Uint index) const { - if (!IsWire()) return m_frontend->GetUniformBlockName(index); const auto* block = Block(index); static const String empty; return block ? block->name : empty; } Uint GetUBOSizeAt(Uint index) const { - if (!IsWire()) return m_frontend->GetUBOSizeAt(index); const auto* block = Block(index); return block ? (static_cast(block->size) + 15u) & ~15u : 0; } Bool IsValidUniformLocation(Int location) const { - return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location); + return Program::IsValidUniformLocation(Link(), location); } Bool UniformLocationsAliasSameUniform(Int a, Int b) const { - if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b); return IsValidUniformLocation(a) && IsValidUniformLocation(b) && Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b]; } GLenum GetUniformType(Uint location) const { - if (!IsWire()) return m_frontend->GetUniformType(location); return IsValidUniformLocation(static_cast(location)) ? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0; } Int GetUniformSamplerOrImageUnitIndex(Uint location) const { - if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location); const auto& units = m_record->SamplerUnits; const auto found = std::lower_bound(units.begin(), units.end(), location, [](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; }); return found != units.end() && found->Location == location ? found->Unit : -1; } Int GetUniformLocation(const String& name) const { - if (!IsWire()) return m_frontend->GetUniformLocation(name); const auto& locations = Link().uniformLocations; auto found = locations.find(name); if (found != locations.end()) return static_cast(found->second); @@ -142,17 +130,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { return UniformLocationsAliasSameUniform(base, location) ? location : -1; } - SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); } + SizeT GetTransformFeedbackVaryingCount() const { return Link().xfbVaryings.size(); } const Vector& GetTransformFeedbackVaryings() const { - return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings(); + return Link().xfbVaryings; } - SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); } + SizeT GetTransformFeedbackBufferCount() const { return Link().xfbStrides.size(); } Uint32 GetTransformFeedbackStride(Uint index) const { - if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index); return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0; } GLenum GetTransformFeedbackBufferMode() const { - return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode(); + return Link().xfbBufferMode; } // P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE. // @@ -164,7 +151,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the // wire draw path entirely - this header no longer includes spirv_reflect.h at all. Uint GetShaderStorageBlockIndex(const String& name) const { - if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name); const auto& blocks = Link().storageBlocks; // The archive carries NORMALISED names (the array subscript stripped), because // that is the spelling SPIRV-Reflect's type_name gives and the spelling @@ -185,7 +171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { : static_cast(found - blocks.begin()); } Uint GetShaderStorageBlockBinding(Uint index) const { - if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index); const auto& blocks = Link().storageBlocks; if (index >= blocks.size()) return 0; const auto& block = blocks[index]; @@ -207,7 +192,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return tIndex >= 0 && static_cast(tIndex) < Link().blockReflection.size() ? &Link().blockReflection[tIndex] : nullptr; } - const Program* m_frontend = nullptr; MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle; const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr; // P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 580b292e..5b50bc15 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -2235,28 +2235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) { - switch (imageTraits.dim) { - case SpvDim1D: - return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D; - case SpvDim2D: - if (imageTraits.ms != 0) { - return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray - : TextureTarget::Texture2DMultisample; - } - return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D; - case SpvDim3D: - return TextureTarget::Texture3D; - case SpvDimCube: - return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap; - case SpvDimBuffer: - return TextureTarget::TextureBuffer; - default: - MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim); - return TextureTarget::Unknown; - } - } - Bool IsFloatStorageImageUniformType(GLenum uniformType) { switch (uniformType) { case GL_IMAGE_1D: @@ -3495,7 +3473,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // contains a dynamic buffer descriptor (VUID 03001/03011). Wire draws // retain UniformManager's per-frame, content-versioned descriptor sets; // this selects the legal ordinary pool, not in-place mutation of a live set. - (!program.IsWire() || entry.dynamicBindings.empty()) && + entry.dynamicBindings.empty() && uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers && updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers && updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers && diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index f1650e49..c8b809b5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -321,30 +321,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } namespace { - constexpr Uint kFallbackTexture2DExternalIndex = 0xFFFFFF00u; - // One id for every storage-image placeholder. They are never reachable through GL - no - // glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the - // id only has to stay clear of the application's, exactly like the sampled fallback's. - constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u; - // The multisample sampled fallbacks: one per (target, numeric domain), because unlike the - // single-sampled fallback they cannot be reinterpreted into another domain at view time - // (see GetFallbackMultisampleTexture). Six reserved ids, contiguous from this base for the - // same reason as the two above - they must not collide with anything glGenTextures can - // hand out. - constexpr Uint kFallbackMultisampleExternalIndexBase = 0xFFFFFF02u; - constexpr Uint kFallbackMultisampleExternalIndexCount = 6u; - - // MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can - // do reaches one, so anything keyed on the GL object an application bound - image-unit - // aliasing above all - has to leave them alone. - Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) { - if (texture == nullptr) return false; - const Uint index = static_cast(texture->GetExternalIndex()); - return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex || - (index >= kFallbackMultisampleExternalIndexBase && - index < kFallbackMultisampleExternalIndexBase + kFallbackMultisampleExternalIndexCount); - } - // The R32 member of each numeric class. Every one of the three is a MANDATORY-support // format for uniform texel buffers, storage texel buffers and storage images alike // (Vulkan 1.0, "Required Format Support"), which is what makes them a fallback that @@ -373,161 +349,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { return (properties.bufferFeatures & requiredFeature) == requiredFeature; } - // Reverse of MG_Util::ConvertTextureInternalFormatToVkEnum. A placeholder texture is - // built through the ordinary frontend texture object (that is what gets it an image with - // STORAGE usage, a GENERAL transition and a view, for free), and that object is described - // by a GL internal format - while everything upstream of here speaks VkFormat. Scanned - // rather than tabulated: it runs once per (target, format) placeholder ever created, the - // enum is ~70 entries, and a second hand-written table is a second thing to drift. - // Ascending order matters: the sized formats precede the unsized aliases, so a scan - // answers with the sized one. - TextureInternalFormat InternalFormatForVkFormat(VkFormat format) { - if (format == VK_FORMAT_UNDEFINED) { - return TextureInternalFormat::Unknown; - } - for (Int index = 0; index < static_cast(TextureInternalFormat::TextureInternalFormatCount); - ++index) { - const auto candidate = static_cast(index); - if (MG_Util::ConvertTextureInternalFormatToVkEnum(candidate) == format) { - return candidate; - } - } - return TextureInternalFormat::Unknown; - } - - // What a 1x1 placeholder of a given target has to allocate for the backend to give it the - // Vulkan view type that target's image declaration demands (see - // VkTextureManager's TryResolveTextureShapeInfo, which reads exactly these two things). - struct PlaceholderShape { - Array uploadTargets{}; - Uint32 uploadTargetCount = 0; - // The GL depth of the single level: the array length for an array target, the depth - // for a 3D one, and 6 for a cube map array (one whole cube). - Int depth = 1; - Bool valid = false; - }; - - PlaceholderShape PlaceholderShapeForTarget(TextureTarget target) { - PlaceholderShape shape{}; - switch (target) { - case TextureTarget::Texture1D: - shape = {{TextureUploadTarget::Texture1D}, 1, 1, true}; - break; - case TextureTarget::Texture2D: - shape = {{TextureUploadTarget::Texture2D}, 1, 1, true}; - break; - case TextureTarget::TextureRectangle: - shape = {{TextureUploadTarget::TextureRectangle}, 1, 1, true}; - break; - case TextureTarget::Texture3D: - shape = {{TextureUploadTarget::Texture3D}, 1, 1, true}; - break; - case TextureTarget::Texture1DArray: - shape = {{TextureUploadTarget::Texture1DArray}, 1, 1, true}; - break; - case TextureTarget::Texture2DArray: - shape = {{TextureUploadTarget::Texture2DArray}, 1, 1, true}; - break; - case TextureTarget::TextureCubeMap: - shape = {{TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX, - TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY, - TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ}, - 6, 1, true}; - break; - case TextureTarget::TextureCubeMapArray: - // Layers are cube faces, so the count must be a whole number of cubes. - shape = {{TextureUploadTarget::CubeMapArray}, 1, 6, true}; - break; - default: - // Multisample targets above all: their descriptor needs a multisample view. - break; - } - return shape; - } - - // TextureObjectMipmap, not ITextureObject: AllocateStorage and MarkStorageDirty live - // there, and every placeholder shape above is one of its subclasses. - SharedPtr MakePlaceholderTextureObject(TextureTarget target, - Uint index) { - switch (target) { - case TextureTarget::Texture1D: - return MakeShared(index); - case TextureTarget::Texture2D: - return MakeShared(index); - case TextureTarget::TextureRectangle: - return MakeShared(index); - case TextureTarget::Texture3D: - return MakeShared(index); - case TextureTarget::Texture1DArray: - return MakeShared(index); - case TextureTarget::Texture2DArray: - return MakeShared(index); - case TextureTarget::TextureCubeMap: - return MakeShared(index); - case TextureTarget::TextureCubeMapArray: - return MakeShared(index); - default: - return nullptr; - } - } } #include "WirePlaceholderImages.inc" - static Bool FindFramebufferAttachmentForTexture(const MG_State::GLState::FramebufferObject& framebuffer, - const MG_State::GLState::ITextureObject& texture, - FramebufferAttachmentType& outAttachment, Int& outLevel) { - const auto& attachments = framebuffer.GetAllAttachmentObjects(); - for (SizeT i = 0; i < attachments.size(); ++i) { - const auto attachmentType = static_cast(i); - if (attachmentType == FramebufferAttachmentType::None) { - continue; - } - - const auto& attachment = attachments[i]; - if (!attachment.IsTexture()) { - continue; - } - - auto attachedTexture = attachment.GetTexture(); - if (attachedTexture && attachedTexture.get() == &texture) { - outAttachment = attachmentType; - outLevel = static_cast(ToStorageMipLevel(attachment.GetTexture().get(), - attachment.GetTextureLevel())); - return true; - } - } - - return false; - } - - static Bool IsValidSampledImageLayout(VkImageLayout layout) { - switch (layout) { - case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_GENERAL: - case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: - return true; - default: - return false; - } - } - - // Uniform location of ELEMENT `element` of the opaque-uniform array at `baseLocation`, or - // -1 when the reflection did not reserve that element. DoReflection hands out one location - // per array element, so the element's location is the base plus its index - bounded by the - // array's real extent so a descriptorCount that outran the reflection cannot walk onto the - // next uniform. Element 0 is the ordinary non-array case and costs nothing extra. - static Int ResolveDescriptorElementLocation(const MagmaProgramSource& program, Int baseLocation, - Uint32 element) { - if (baseLocation < 0 || element == 0) { - return baseLocation; - } - const Int location = baseLocation + static_cast(element); - return program.UniformLocationsAliasSameUniform(baseLocation, location) ? location : -1; - } - // descriptorCount this binding declares in the descriptor set layout (1 for everything that // is not an array). Kept in one place because the layout, the scratch reservation and the // per-element write loops must all agree on it. @@ -537,18 +362,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { : 1u; } - static Int ResolveSamplerUnitIndex(const MagmaProgramSource& program, Int location, Uint32 binding) { - MOBILEGL_ASSERT(location >= -1, "ResolveSamplerUnitIndex: invalid sampler location for binding %u", binding); - if (location < 0) { - return 0; - } - const Int uniformUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - MOBILEGL_ASSERT(uniformUnit >= -1, - "ResolveSamplerUnitIndex: invalid texture unit for binding %u location %d (unit=%d)", binding, - location, uniformUnit); - return uniformUnit >= 0 ? uniformUnit : 0; - } - VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, VkFormat resourceFormat, Bool useBindingFormat) { if (useBindingFormat) { @@ -645,7 +458,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } m_unboundTexelBufferViews.clear(); - m_unboundStorageImageTextures.clear(); for (auto& frame : m_frames) { if (m_device != VK_NULL_HANDLE) { frame.wireImageViewCache.clear(); @@ -693,8 +505,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_peakDescriptorSetsObserved = 0; m_textureManager = nullptr; m_samplerManager = nullptr; - m_fallbackTexture2D.reset(); - m_fallbackMultisampleTextures.clear(); } void UniformManager::BeginFrame(Uint32 frameIndex) { @@ -891,246 +701,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo, Bool trustUnchangedHint) const { - if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo); - MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null"); - MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null"); - // The whole-descriptor memo below is keyed by binding alone, so it describes a binding - // that carries exactly one descriptor. An arrayed binding's elements would overwrite - // each other in it (see SamplerResolveMemo::info); they re-resolve instead. - const Bool descriptorMemoUsable = BindingDescriptorCount(programObj, binding) == 1u; - // The caller proved every input of this binding's resolution unchanged since the - // last full resolve (which also filled the cache), so the whole chain below - - // texture/sampler resolution, completeness probe, sync, layout handling, sampler - // and view lookups - would recompute the identical descriptor. - if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() && - m_samplerResolveMemo[binding].infoValid && - m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) { - outImageInfo = m_samplerResolveMemo[binding].info; - return true; - } - MOBILEGL_ASSERT(binding < programObj.samplerNameByBinding.size(), - "ResolveSamplerDescriptor: sampler binding %u name lookup out of range", binding); - // Per ELEMENT, and resolved BEFORE anything is looked up through it: GLSL 4.20 gives every - // element of `uniform sampler2D goku[4]` its own texture unit (consecutive from the - // declared binding, but glUniform1i may scatter them afterwards), so the unit - and with - // it the bound texture, the unit's sampler override and the fallback decision - is the - // element's, not the binding's. An element past the array's reserved extent has no unit - // at all, and must not fall back to resolving unit 0's texture. - const Int location = - ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element); - if (location < 0 && element > 0) { - MGLOG_D("ResolveSamplerDescriptor: binding %u element %u is past the end of its sampler array", binding, - element); - return false; - } - const Int unit = ResolveSamplerUnitIndex(program, location, binding); - // Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays - // alive through the draw via GL binding state. Only the fallback path needs a SharedPtr to - // keep the fallback texture alive for the rest of this call. - MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding, element); - auto& textureUnit = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit); - const auto& samplerOverride = textureUnit.GetSamplerObject(); - const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding]; - SharedPtr fallbackHolder; - // A texture that fails the completeness rules for the filter in effect reads - // (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the - // same route as a sampler with nothing bound. - if (texture != nullptr && - MG_State::GLState::SamplesAsIncompleteTexture( - texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) { - texture = nullptr; - } - if (texture == nullptr) { - // The binding's sampler class, read here rather than through the `numericDomain` - // local further down (it is declared after this point): the multisample placeholder - // has to be built in the class the shader will read it in. - fallbackHolder = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]); - texture = fallbackHolder.get(); - if (texture == nullptr) { - MGLOG_E_ONCE("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') " - "location=%d unit=%d target=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), location, unit, - static_cast(preferredTarget)); - return false; - } - MGLOG_W_ONCE( - "ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), location, unit, - static_cast(preferredTarget)); - } - - const MG_State::GLState::SamplerObject* samplerToUse = - samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get(); - if (samplerToUse == nullptr) { - MGLOG_E_ONCE( - "ResolveSamplerDescriptor: sampler binding %u ('%s') has no sampler object (textureId=%d location=%d unit=%d)", - binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), location, - unit); - return false; - } - VkTextureManager::TextureResource* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); - if (resource == nullptr) { - MGLOG_E_ONCE( - "ResolveSamplerDescriptor: sampler binding %u ('%s') failed to create/sync texture resource (textureId=%d target=%d location=%d unit=%d)", - binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), - static_cast(texture->GetTarget()), location, unit); - return false; - } - if (!IsValidSampledImageLayout(resource->layout)) { - auto drawFbo = MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); - FramebufferAttachmentType attachmentType = FramebufferAttachmentType::None; - Int attachmentLevel = 0; - if (drawFbo && - FindFramebufferAttachmentForTexture(*drawFbo, *texture, attachmentType, attachmentLevel)) { - MGLOG_W_ONCE("ResolveSamplerDescriptor: framebuffer feedback loop detected: textureId=%d is bound " - "for sampling at binding=%u, but is also attached to drawFbo=%u as %s (level=%d, " - "trackedLayout=%d)", - texture->GetExternalIndex(), binding, drawFbo->GetExternalIndex(), - MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(), - attachmentLevel, static_cast(resource->layout)); - } - - const Bool readyForSampling = m_textureManager->TransitionTextureForSampling(commandBuffer, *texture); - if (!readyForSampling) { - MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to transition textureId=%d for sampler binding=%u", - texture->GetExternalIndex(), binding); - return false; - } - resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); - MOBILEGL_ASSERT(resource != nullptr, - "ResolveSamplerDescriptor: failed to resync textureId=%d after sampling transition", - texture->GetExternalIndex()); - MOBILEGL_ASSERT(IsValidSampledImageLayout(resource->layout), - "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", - static_cast(resource->layout), texture->GetExternalIndex(), binding); - } - - MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(), - "ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding); - const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding]; - // Vulkan forbids linear filtering and anisotropy for integer sampled-image formats. - // Some desktop GL shader packs deliberately bit-read a mutable float texture through a - // usampler and still leave the texture's ordinary linear parameters in place; texelFetch - // ignores filtering, so a nearest VkSampler preserves the operation while keeping the - // descriptor valid. - const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger || - numericDomain == SamplerNumericDomain::UnsignedInteger; - SamplerResolveMemo* viewFormatMemo = - binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr; - // The format this GL texture presents to the shader. For a texture created by - // glTextureView that is the format the VIEW reinterpreted its storage as (GL 4.6 core - // 8.18), not the storage image's own - resolving the numeric domain against the latter - // would pick a sampled view for a format the shader never declared. The probe is behind - // IsTextureView() so nothing about the ordinary per-draw path changes. - const VkFormat sampledSourceFormat = - texture->IsTextureView() - ? m_textureManager->ResolveTextureViewWindow(*texture, *resource).format - : resource->format; - VkFormat sampledViewFormat; - if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid && - viewFormatMemo->viewFormatSource == sampledSourceFormat && - viewFormatMemo->viewFormatDomain == numericDomain) { - sampledViewFormat = viewFormatMemo->viewFormat; - } else { - sampledViewFormat = - VkTextureManager::ResolveSampledImageViewFormat(sampledSourceFormat, numericDomain); - if (viewFormatMemo != nullptr) { - viewFormatMemo->viewFormatSource = sampledSourceFormat; - viewFormatMemo->viewFormatDomain = numericDomain; - viewFormatMemo->viewFormat = sampledViewFormat; - viewFormatMemo->viewFormatValid = true; - NoteSamplerResolveMemoTouched(binding); - } - } - if (sampledViewFormat == VK_FORMAT_UNDEFINED) { - MGLOG_E_ONCE("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') " - "textureId=%d imageFormat=%d numericDomain=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), - static_cast(resource->format), static_cast(numericDomain)); - return false; - } - // No reinterpretation requested: bind the depth-or-color aspect view the sync above - // already produced instead of re-entering GetOrCreateSampledImageView's sync path. A GL - // texture view is excluded because resource->sampledView belongs to the texture it VIEWS - // - same image, but the storage texture's level range and depth/stencil aspect, which is - // exactly the state a view exists to differ on. - const VkImageView sampledImageView = - (!texture->IsTextureView() && sampledViewFormat == resource->format) - ? resource->sampledView - : m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat); - if (sampledImageView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') " - "textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), - static_cast(resource->format), static_cast(sampledViewFormat), - static_cast(numericDomain)); - return false; - } - // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's - // sampler object and texture (both by lifetime id + version) are unchanged from the - // last draw that resolved it: the resulting sampler key, and therefore the VkSampler - // handle, are guaranteed identical. Lifetime ids are never reused, so a freed-and- - // reallocated sampler/texture at the same address gets a fresh id and misses instead - // of false-hitting. Cached handles live until Shutdown, so the memo can never hand - // back a destroyed sampler. - VkSampler resolvedSampler = VK_NULL_HANDLE; - if (binding < m_samplerResolveMemo.size()) { - auto& memo = m_samplerResolveMemo[binding]; - const Uint64 samplerLifetimeId = samplerToUse->GetLifetimeId(); - const Uint16 samplerVersion = samplerToUse->GetVersion(); - const Uint64 textureLifetimeId = texture->GetLifetimeId(); - const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); - // The sampler's LOD clamp depends on how many levels the sampled view exposes, and that - // follows uploads as well as GL parameters - so it belongs in the memo key too. - const Uint32 viewLevelCount = resource->sampledLevelCount; - if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && - memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion && - memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) { - resolvedSampler = memo.sampler; - } else { - resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, - forceNearestFiltering, viewLevelCount); - memo.samplerLifetimeId = samplerLifetimeId; - memo.samplerVersion = samplerVersion; - memo.textureLifetimeId = textureLifetimeId; - memo.textureParamsVersion = textureParamsVersion; - memo.forceNearestFiltering = forceNearestFiltering; - memo.viewLevelCount = viewLevelCount; - memo.sampler = resolvedSampler; - memo.valid = true; - NoteSamplerResolveMemoTouched(binding); - } - } else { - resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering, - resource->sampledLevelCount); - } - outImageInfo = { - .sampler = resolvedSampler, - .imageView = sampledImageView, - .imageLayout = resource->layout, - }; - if (outImageInfo.sampler == VK_NULL_HANDLE) { - return false; - } - // Only for a binding that carries a single descriptor - an array's elements would - // publish each other's descriptors here, and the next hinted draw would hand element - // N-1's texture to element 0. - if (binding < m_samplerResolveMemo.size()) { - if (descriptorMemoUsable) { - m_samplerResolveMemo[binding].info = outImageInfo; - m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId(); - m_samplerResolveMemo[binding].infoValid = true; - } else { - // An arrayed binding publishes nothing here, and clears what a previous program - // published at this index. Not strictly required - the hint's proof obligations - // are program-scoped and the entry is reset every frame - but leaving another - // program's descriptor sitting in a slot this one never refreshes is the kind of - // thing the next reader has to re-derive is safe. - m_samplerResolveMemo[binding].infoValid = false; - } - NoteSamplerResolveMemoTouched(binding); - } - return true; + return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, false, outImageInfo); } Bool UniformManager::ResolveSamplerDescriptorOverride( @@ -1172,119 +743,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return outImageInfo.sampler != VK_NULL_HANDLE; } - Bool UniformManager::ProgramSamplesOnlySingleLevelTextures( - const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { - if (program.IsWire()) return false; // Conservative optimization gate; no frontend probe. - // A declined program never draws (see VkProgramObject::declinedDescriptors), and its - // declined binding has no resolvable uniform location - so there is nothing to prove - // about the textures it would have sampled. - if (programObj.declinedDescriptors) { - return false; - } - Bool sawSampler = false; - for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) { - if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { - continue; - } - // The rewrite this gates is program-wide, so EVERY sampler the program can read has - // to qualify - including every element of a sampler array, each of which reaches a - // different texture through its own unit. - const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); - for (Uint32 element = 0; element < descriptorCount; ++element) { - // The element's own location first, exactly as ResolveSamplerDescriptor resolves - // it - an element with no location would otherwise be judged on unit 0's texture. - const Int location = ResolveDescriptorElementLocation( - program, programObj.samplerUniformLocationByBinding[binding], element); - if (location < 0 && element > 0) return false; - const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding, element); - if (texture == nullptr) return false; - const auto& levelRange = texture->GetLevelRange(); - if (levelRange.x() != levelRange.y()) return false; - - // An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic - // filtering - which a single-level view can still have. Resolve the sampler exactly - // the way ResolveSamplerDescriptor does and bail if anisotropy would apply. - const Int unit = ResolveSamplerUnitIndex(program, location, binding); - const auto& samplerOverride = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit).GetSamplerObject(); - const auto* effectiveSampler = - samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get(); - if (effectiveSampler == nullptr) return false; - if (effectiveSampler->GetMaxAnisotropy() > 1.0f && - effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear && - effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) { - return false; - } - - // An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the - // min/mag decision. That only matches the implicit form when lambda could not have been - // positive anyway (the LOD clamp already pins it at or below 0), or when the two - // filters are the same and the choice cannot be observed. - const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None - ? 0.0f - : effectiveSampler->GetMaxLod(); - if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) { - return false; - } - sawSampler = true; - } - } - return sawSampler; - } - - Bool UniformManager::ResolveSamplerTexture(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - SharedPtr& outTexture) { - outTexture.reset(); - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveSamplerTexture: GL context is null"); - MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), - "ResolveSamplerTexture: sampler location binding %u out of range", binding); - MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), - "ResolveSamplerTexture: sampler target binding %u out of range", binding); - - const Int location = programObj.samplerUniformLocationByBinding[binding]; - const Int unit = ResolveSamplerUnitIndex(program, location, binding); - - auto& textureUnit = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit); - const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; - outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject(); - // The slot always holds at least the target's default texture (name 0). While that - // default has no image it is unsampleable; report it as "unbound" so callers keep - // taking their fallback paths instead of trying to sync a storage-less texture. - if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) { - outTexture.reset(); - } - - return true; - } - - MG_State::GLState::ITextureObject* UniformManager::ResolveSamplerTextureRaw( - const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, - Uint32 binding, Uint32 element) { - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveSamplerTextureRaw: GL context is null"); - MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), - "ResolveSamplerTextureRaw: sampler location binding %u out of range", binding); - MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), - "ResolveSamplerTextureRaw: sampler target binding %u out of range", binding); - - const Int location = - ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element); - const Int unit = ResolveSamplerUnitIndex(program, location, binding); - - auto& textureUnit = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit); - const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; - // GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without - // touching the refcount (no atomic inc/dec per binding per draw). - MG_State::GLState::ITextureObject* texture = - textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get(); - // The slot always holds at least the target's default texture (name 0). While that - // default has no image it is unsampleable; report it as "unbound" so the caller - // substitutes its fallback texture exactly like it did for the old null slot. - if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) { - return nullptr; - } - return texture; - } - Bool UniformManager::ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, Bool storage, VkBufferView& out) { @@ -1391,114 +849,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { - if (program.IsWire()) - return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, false, outBufferView); - outBufferView = VK_NULL_HANDLE; - MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveTexelBufferDescriptor: buffer manager is null"); - MOBILEGL_ASSERT(frameIndex < m_frames.size(), "ResolveTexelBufferDescriptor: frame index out of range"); - - MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(), - "ResolveTexelBufferDescriptor: numeric domain binding %u out of range", binding); - const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding]; - - SharedPtr texture; - if (!ResolveSamplerTexture(program, programObj, binding, texture) || texture == nullptr) { - // NOT an error, and not a reason to lose the draw. A texture unit with nothing on it - // is a legal GL state (4.6 core 8.24): the sampler is incomplete, so a fetch through - // it returns undefined values - the same answer the sampled path above gives with its - // fallback texture, which a buffer texture simply cannot use because its descriptor is - // a VkBufferView. A per-format placeholder view is the equivalent for this kind. - const VkBufferView placeholder = - AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false); - if (placeholder == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') is unbound, and the " - "placeholder descriptor could not be created", binding, - programObj.samplerNameByBinding[binding].c_str()); - return false; - } - MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') is unbound; using the placeholder descriptor", - binding, programObj.samplerNameByBinding[binding].c_str()); - outBufferView = placeholder; - return true; - } - - if (texture->GetStorageType() != TextureStorageType::Buffer || - texture->GetTarget() != TextureTarget::TextureBuffer) { - MGLOG_E_ONCE( - "ResolveTexelBufferDescriptor: binding %u ('%s') expected texture buffer, got textureId=%u target=%d storage=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(), - static_cast(texture->GetTarget()), static_cast(texture->GetStorageType())); - return false; - } - - auto* textureBuffer = static_cast(texture.get()); - const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject(); - if (bufferObject == nullptr) { - // A buffer texture with no buffer object attached is INCOMPLETE, not illegal (GL 4.6 - // core 8.9), and sampling an incomplete texture is undefined - so this too keeps the - // draw on a placeholder rather than dropping it. - const VkBufferView placeholder = - AcquireUnboundTexelBufferView(VK_FORMAT_UNDEFINED, numericDomain, false); - if (placeholder == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer binding %u ('%s') has no GL buffer bound, " - "and the placeholder descriptor could not be created", binding, - programObj.samplerNameByBinding[binding].c_str()); - return false; - } - MGLOG_D("ResolveTexelBufferDescriptor: binding %u ('%s') has no attached GL buffer; using the " - "placeholder descriptor", binding, programObj.samplerNameByBinding[binding].c_str()); - outBufferView = placeholder; - return true; - } - - BufferSlice slice{}; - if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || !slice.IsValid()) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u", - bufferObject->GetExternalIndex(), texture->GetExternalIndex()); - return false; - } - - const auto internalFormat = textureBuffer->GetFormat(); - const VkFormat vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat); - if (vkFormat == VK_FORMAT_UNDEFINED) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: unsupported texture buffer internal format %d", - static_cast(internalFormat)); - return false; - } - - const VkDeviceSize texelSize = - static_cast(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat)); - // glTextureBufferRange addresses a window of the buffer, not all of it; the whole-buffer - // forms report the buffer's current size here, so both go through the same clamp. - const VkDeviceSize rangeOffset = static_cast(textureBuffer->GetBufferRangeOffset()); - const VkDeviceSize rangeSize = static_cast(textureBuffer->GetBufferRangeSizeInBytes()); - VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0); - if (texelSize > 0) { - viewRange = (viewRange / texelSize) * texelSize; - } - if (viewRange == 0) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: texture buffer %u has empty view range", texture->GetExternalIndex()); - return false; - } - - VkBufferViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; - viewInfo.buffer = slice.buffer; - viewInfo.format = vkFormat; - viewInfo.offset = slice.offset + rangeOffset; - viewInfo.range = viewRange; - - VkBufferView bufferView = VK_NULL_HANDLE; - const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView); - if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu", - result, static_cast(vkFormat), static_cast(viewRange)); - return false; - } - - m_frames[frameIndex].texelBufferViews.push_back(bufferView); - outBufferView = bufferView; - return true; + return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, false, outBufferView); } // GLSL `imageBuffer`. The one image uniform whose Vulkan descriptor is a VkBufferView rather @@ -1517,164 +868,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView) { - if (program.IsWire()) - return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, true, outBufferView); - outBufferView = VK_NULL_HANDLE; - MOBILEGL_ASSERT(m_bufferManager != nullptr, "ResolveStorageTexelBufferDescriptor: buffer manager is null"); - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveStorageTexelBufferDescriptor: GL context is null"); - MOBILEGL_ASSERT(frameIndex < m_frames.size(), - "ResolveStorageTexelBufferDescriptor: frame index out of range"); - MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), - "ResolveStorageTexelBufferDescriptor: binding %u out of range", binding); - - const Int location = programObj.samplerUniformLocationByBinding[binding]; - if (location < 0) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') has no uniform location", binding, - programObj.samplerNameByBinding[binding].c_str()); - return false; - } - const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d out of range for binding %u", imageUnit, - binding); - return false; - } - - MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), - "ResolveStorageTexelBufferDescriptor: binding %u has no reflected format slot", binding); - MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(), - "ResolveStorageTexelBufferDescriptor: numeric domain binding %u out of range", binding); - - auto& imageBinding = MG_Pipe::gPipeInputs.GetImageTextureBinding(imageUnit); - const auto& texture = imageBinding.Texture; - if (texture == nullptr) { - // An image unit with no texture on it is legal GL (4.6 core 8.26): loads return zero - // and stores are discarded. Declining here took the whole draw or dispatch with it - - // the same shape as the unbound storage block fixed alongside this. A placeholder view - // in the shader's own declared format lets the work proceed with the stores landing - // nowhere anyone can observe, which is what GL asks for. - const VkBufferView placeholder = - AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding], - programObj.samplerNumericDomainByBinding[binding], true); - if (placeholder == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: image unit %d is unbound for binding %u, and the " - "placeholder descriptor could not be created", imageUnit, binding); - return false; - } - MGLOG_D("ResolveStorageTexelBufferDescriptor: image unit %d (binding %u) is unbound; using the " - "placeholder descriptor", imageUnit, binding); - outBufferView = placeholder; - return true; - } - if (texture->GetStorageType() != TextureStorageType::Buffer || - texture->GetTarget() != TextureTarget::TextureBuffer) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: binding %u ('%s') expected a texture buffer on image " - "unit %d, got textureId=%u target=%d storage=%d", - binding, programObj.samplerNameByBinding[binding].c_str(), imageUnit, - texture->GetExternalIndex(), static_cast(texture->GetTarget()), - static_cast(texture->GetStorageType())); - return false; - } - - auto* textureBuffer = static_cast(texture.get()); - const auto& bufferObject = textureBuffer->GetBufferBindingSlot().GetBoundObject(); - if (bufferObject == nullptr) { - // Incomplete buffer texture, same as the sampled path: legal state, undefined data, - // and no reason to drop the work. - const VkBufferView placeholder = - AcquireUnboundTexelBufferView(programObj.storageImageFormatByBinding[binding], - programObj.samplerNumericDomainByBinding[binding], true); - if (placeholder == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no GL buffer " - "bound, and the placeholder descriptor could not be created", imageUnit); - return false; - } - MGLOG_D("ResolveStorageTexelBufferDescriptor: texture buffer on image unit %d has no attached GL buffer; " - "using the placeholder descriptor", imageUnit); - outBufferView = placeholder; - return true; - } - - // Unlike the sampled texel buffer, the shader MAY write this one, and those writes land - // in GPU memory behind the frontend's CPU shadow - which is what MapBuffer and - // GetBufferSubData read. Same two calls, and for the same reason, as the storage-block - // path above - but only the residency is unconditional. Marking a GL_READ_ONLY binding - // GPU-written would make the next map or readback wait for a dispatch that could not have - // changed a byte of it. - bufferObject->EnsureGpuResidentStorage(); - if (imageBinding.Access != GL_READ_ONLY) { - // P5c ev (R2, CONTRACT-P5C §4.2): the announcement goes through the reverse - // channel - the apply thread may not poke the client object directly. - MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject); - } - - BufferSlice slice{}; - if (!m_bufferManager->AcquireResidentSlice(BufferKind::TextureBuffer, bufferObject, slice) || - !slice.IsValid()) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: failed to sync GL buffer %u for texture buffer %u", - bufferObject->GetExternalIndex(), texture->GetExternalIndex()); - return false; - } - - // The format the SHADER declared wins over the one glBindImageTexture named, on the same - // policy as a storage image: a typed `layout(r32ui) uniform uimageBuffer` must be read as - // r32ui whatever the texture's own attachment format says. Falling back, in order: - // reflected format, then the bind format, then the texture's attached format. - const auto internalFormat = textureBuffer->GetFormat(); - const VkFormat resourceFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat); - const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding]; - VkFormat vkFormat = reflectedFormat; - if (vkFormat == VK_FORMAT_UNDEFINED && imageBinding.Format != 0) { - vkFormat = MG_Util::ConvertTextureInternalFormatToVkEnum( - MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format)); - } - if (vkFormat == VK_FORMAT_UNDEFINED) { - vkFormat = resourceFormat; - } - if (vkFormat == VK_FORMAT_UNDEFINED) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: unsupported image buffer format (internal=%d bind=0x%x)", - static_cast(internalFormat), imageBinding.Format); - return false; - } - - // Sized from the TEXTURE's attached format even though the view may carry a different - // one. That is not a shortcut: GL requires the shader's format qualifier, the format - // passed to glBindImageTexture and the texture's own internal format to belong to the - // same format CLASS (GL 4.6 core, table 8.27), and every member of a class has the same - // texel size. So the three can disagree on interpretation and never on bytes - which is - // what the range below has to be a whole multiple of. - const VkDeviceSize texelSize = - static_cast(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat)); - const VkDeviceSize rangeOffset = static_cast(textureBuffer->GetBufferRangeOffset()); - const VkDeviceSize rangeSize = static_cast(textureBuffer->GetBufferRangeSizeInBytes()); - VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0); - if (texelSize > 0) { - viewRange = (viewRange / texelSize) * texelSize; - } - if (viewRange == 0) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: texture buffer %u has empty view range", - texture->GetExternalIndex()); - return false; - } - - VkBufferViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; - viewInfo.buffer = slice.buffer; - viewInfo.format = vkFormat; - viewInfo.offset = slice.offset + rangeOffset; - viewInfo.range = viewRange; - - VkBufferView bufferView = VK_NULL_HANDLE; - const VkResult result = vkCreateBufferView(m_device, &viewInfo, nullptr, &bufferView); - if (result != VK_SUCCESS || bufferView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveStorageTexelBufferDescriptor: vkCreateBufferView failed result=%d format=%d range=%zu", - result, static_cast(vkFormat), static_cast(viewRange)); - return false; - } - - m_frames[frameIndex].texelBufferViews.push_back(bufferView); - outBufferView = bufferView; - return true; + return ResolveWireTexelBufferDescriptor(program, programObj, binding, frameIndex, true, outBufferView); } Bool UniformManager::ResolveStorageBufferDescriptor(const MagmaProgramSource& program, @@ -1686,14 +880,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveStorageBufferDescriptor: GL context is null"); MOBILEGL_ASSERT(binding < programObj.storageBlockIndexByBinding.size(), "ResolveStorageBufferDescriptor: binding %u out of range", binding); - if (program.IsWire() && (binding >= programObj.storageBlockIndexByBinding.size() || + if ((binding >= programObj.storageBlockIndexByBinding.size() || binding >= programObj.storageBlockNameByBinding.size())) WireDescriptorFatal("storage-buffer-reflection-binding"); const Int blockIndex = programObj.storageBlockIndexByBinding[binding]; MOBILEGL_ASSERT(blockIndex >= 0, "ResolveStorageBufferDescriptor: no SSBO block mapped to binding %u", binding); - if (program.IsWire() && blockIndex < 0) WireDescriptorFatal("storage-buffer-reflection-block"); + if (blockIndex < 0) WireDescriptorFatal("storage-buffer-reflection-block"); // An atomic counter is not an SSBO the application ever declared: glslang lowers every // atomic_uint onto a synthesized gl_AtomicCounterBlock_ storage block, where N is the // GL ATOMIC-COUNTER binding. That block arrives here auto-mapped to an arbitrary @@ -1722,154 +916,79 @@ namespace MobileGL::MG_Backend::DirectVulkan { const GLuint frontendBinding = isAtomicCounterBlock ? static_cast(atomicCounterBinding) - : - (program.IsWire() ? program.GetShaderStorageBlockBinding(static_cast(blockIndex)) : - GetShaderStorageBlockBinding(*program.Frontend(), static_cast(blockIndex))) + element; - if (program.IsWire()) { - const auto& state = MG_Pipe::MGPipeApplier(); - const Uint32 cls = isAtomicCounterBlock ? MG_Pipe::kMGPipeShaderBufferClassAtomicCounter - : MG_Pipe::kMGPipeShaderBufferClassShaderStorage; - if (frontendBinding >= MG_Pipe::kMGPipeMaxBufferBindingPoints) - WireDescriptorFatal("storage-buffer-binding-point"); - // The applier retains entries outside the last partial update window. Count - // describes that update, not the upper bound of the complete binding table. - const auto& range = state.BoundShaderBuffers[cls][frontendBinding]; - if (MG_Pipe::MGPipeHandleIsNull(range.Res)) { - const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); - if (!placeholder.IsValid()) return false; - outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; - return true; - } - BufferSlice slice{}; - if (!m_bufferManager->AcquireWireSlice(BufferKind::ShaderStorage, range.Res, slice) || !slice.IsValid()) return false; - VkDeviceSize start = 0, size = 0; - if (!ResolveWireRange(range.Offset, range.Size, slice.size, start, size)) return false; - if (start > std::numeric_limits::max() - slice.offset) - WireDescriptorFatal("storage-buffer-offset-overflow"); - if ((slice.offset + start) % m_wireStorageOffsetAlignment != 0) { - // P7 A.4, a named decline (rule I (a)), and the one shape in this cluster an - // application can actually reach. GL has no queryable atomic-counter buffer - // offset alignment, so glBindBufferRange only enforces offset % 4 for - // GL_ATOMIC_COUNTER_BUFFER (GL 4.6 core 6.1.1, GL_Buffer.cpp - // ValidateBufferRangeOffsetAndSize); glslang lowers the counter block onto a - // storage buffer, whose descriptor offset must be a multiple of - // minStorageBufferOffsetAlignment - 16 on lavapipe, 64 on Adreno. Offset 4 is - // therefore a legal bind this backend cannot express. The plain SSBO half is - // unreachable by comparison: GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is - // published from that same Vulkan limit, so the frontend already refused it - // with GL_INVALID_VALUE. - // - // DECLINED RATHER THAN COPIED, deliberately. Copying the window into an aligned - // transient slice reads correctly and then silently DROPS every shader write, - // and this site cannot tell a writable block from a read-only one: there is no - // readonly bit in the storage-block reflection (ProgramFactory.h carries only - // name and index), an atomic counter is written by definition, and - // MarkWireBufferGpuWritten below already assumes any binding here may be - // written. A correct copy needs a post-dispatch copy-back from the transient - // slice to the unaligned source, ordered with a barrier - and the place to - // record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.inc, - // which belong to wave-2 packages B and C, not to this file. Losing the - // dispatch with a named line in the role log beats returning stale counters - // with no error at all. notes/p7/magma-a.md carries the recipe. - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' is bound at offset %llu, which is not a " - "multiple of this device's minStorageBufferOffsetAlignment (%llu); %s ranges cannot be " - "expressed as a descriptor and the shader's writes would be dropped by a read-only " - "copy, so the binding is DECLINED (unaligned-%s-buffer-range)", - blockName.c_str(), static_cast(slice.offset + start), - static_cast(m_wireStorageOffsetAlignment), - isAtomicCounterBlock ? "atomic-counter" : "shader-storage", - isAtomicCounterBlock ? "atomic" : "storage"); - return false; - } - if (size > m_wireMaxStorageRange) { - // P7 A.3: clamp, do not refuse. The monolith arm binds whatever the GL range - // asked for and never consults maxStorageBufferRange at all (see the resident - // path below), so refusing here was the wire arm inventing a death the other - // backend does not have. A clamped binding keeps every byte the device can - // name reachable; past it the shader is out of the descriptor and - // robustBufferAccess answers zero, which beats losing the draw outright. - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' bound %llu bytes, past this device's " - "maxStorageBufferRange of %llu; the binding is clamped to the limit", - blockName.c_str(), static_cast(size), - static_cast(m_wireMaxStorageRange)); - size = m_wireMaxStorageRange; - } - outBufferInfo = {slice.buffer, slice.offset + start, size}; - // Shader writes stay in the canonical server VkBuffer. The event marks the - // client's shadow stale; neither this path nor later acquires seed it back. - m_bufferManager->MarkWireBufferGpuWritten(range.Res, start, size); - return true; - } - const Uint32 bindingPointCount = - static_cast(MG_Pipe::gPipeInputs.GetBufferBindingPointCount(bufferTarget)); - MOBILEGL_ASSERT(frontendBinding < bindingPointCount, - "ResolveStorageBufferDescriptor: frontend binding %u out of range for block '%s'", - frontendBinding, blockName.c_str()); - - auto& bindingPoint = MG_Pipe::gPipeInputs.GetBufferBindingPoint(bufferTarget, frontendBinding); - const auto& bufferObject = bindingPoint.GetBoundObject(); - if (bufferObject == nullptr) { - // NOT an error, and above all not a reason to lose the draw. GL 4.6 core 7.8 lets a - // program declare a shader storage block the application never binds a buffer to: - // the block simply has no store, so a read is undefined and a write goes nowhere. - // Refusing here used to take the whole draw or dispatch with it (SetupDraw and - // DispatchCompute both skip on a false return), which is how AcceleratedRendering's - // GUI batch lost its backgrounds: its vertex-transform compute shader declares a - // `Meshes` block it only reads when a vertex comes from a cached server mesh, and a - // batch of plain GUI blits has no meshes and so binds nothing there. The dispatch - // never ran, the transformed vertex buffer stayed as it was, and every hotbar and - // container-screen background quad came out degenerate. A shared zero-filled - // placeholder puts something legal in the descriptor and lets the draw proceed. + : program.GetShaderStorageBlockBinding(static_cast(blockIndex)) + element; + const auto& state = MG_Pipe::MGPipeApplier(); + const Uint32 cls = isAtomicCounterBlock ? MG_Pipe::kMGPipeShaderBufferClassAtomicCounter + : MG_Pipe::kMGPipeShaderBufferClassShaderStorage; + if (frontendBinding >= MG_Pipe::kMGPipeMaxBufferBindingPoints) + WireDescriptorFatal("storage-buffer-binding-point"); + // The applier retains entries outside the last partial update window. Count + // describes that update, not the upper bound of the complete binding table. + const auto& range = state.BoundShaderBuffers[cls][frontendBinding]; + if (MG_Pipe::MGPipeHandleIsNull(range.Res)) { const BufferSlice placeholder = m_bufferManager->AcquireUnboundStorageDescriptor(); - if (!placeholder.IsValid()) { - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: no buffer bound at frontend binding %u for block " - "'%s', and the placeholder descriptor could not be created", - frontendBinding, blockName.c_str()); - return false; - } - MGLOG_D("ResolveStorageBufferDescriptor: frontend binding %u ('%s') is unbound; using the placeholder " - "descriptor", - frontendBinding, blockName.c_str()); - outBufferInfo.buffer = placeholder.buffer; - outBufferInfo.offset = placeholder.offset; - outBufferInfo.range = placeholder.size; + if (!placeholder.IsValid()) return false; + outBufferInfo = {placeholder.buffer, placeholder.offset, placeholder.size}; return true; } - - // The shader may write this buffer, and those writes land in GPU memory behind the - // frontend's CPU shadow - which is what MapBuffer and GetBufferSubData read. - // Host-visible coherent GPU residency makes the shadow BE that memory, so the - // results are visible without a readback path, exactly as for a capture buffer. - bufferObject->EnsureGpuResidentStorage(); - // ... and the read that follows has to wait for this draw or dispatch to retire. - // P5c ev (R2, CONTRACT-P5C §4.2): through the reverse channel, not a direct poke of - // the client object from the apply thread. - MG_Pipe::MGPipeAnnounceBufferGpuWritten(bufferObject); - BufferSlice slice{}; - if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) { - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'", - bufferObject->GetExternalIndex(), programObj.storageBlockNameByBinding[binding].c_str()); + if (!m_bufferManager->AcquireWireSlice(BufferKind::ShaderStorage, range.Res, slice) || !slice.IsValid()) return false; + VkDeviceSize start = 0, size = 0; + if (!ResolveWireRange(range.Offset, range.Size, slice.size, start, size)) return false; + if (start > std::numeric_limits::max() - slice.offset) + WireDescriptorFatal("storage-buffer-offset-overflow"); + if ((slice.offset + start) % m_wireStorageOffsetAlignment != 0) { + // P7 A.4, a named decline (rule I (a)), and the one shape in this cluster an + // application can actually reach. GL has no queryable atomic-counter buffer + // offset alignment, so glBindBufferRange only enforces offset % 4 for + // GL_ATOMIC_COUNTER_BUFFER (GL 4.6 core 6.1.1, GL_Buffer.cpp + // ValidateBufferRangeOffsetAndSize); glslang lowers the counter block onto a + // storage buffer, whose descriptor offset must be a multiple of + // minStorageBufferOffsetAlignment - 16 on lavapipe, 64 on Adreno. Offset 4 is + // therefore a legal bind this backend cannot express. The plain SSBO half is + // unreachable by comparison: GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is + // published from that same Vulkan limit, so the frontend already refused it + // with GL_INVALID_VALUE. + // + // DECLINED RATHER THAN COPIED, deliberately. Copying the window into an aligned + // transient slice reads correctly and then silently DROPS every shader write, + // and this site cannot tell a writable block from a read-only one: there is no + // readonly bit in the storage-block reflection (ProgramFactory.h carries only + // name and index), an atomic counter is written by definition, and + // MarkWireBufferGpuWritten below already assumes any binding here may be + // written. A correct copy needs a post-dispatch copy-back from the transient + // slice to the unaligned source, ordered with a barrier - and the place to + // record that is the draw/dispatch tail in VulkanRenderer.cpp / WireDraw.inc, + // which belong to wave-2 packages B and C, not to this file. Losing the + // dispatch with a named line in the role log beats returning stale counters + // with no error at all. notes/p7/magma-a.md carries the recipe. + MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' is bound at offset %llu, which is not a " + "multiple of this device's minStorageBufferOffsetAlignment (%llu); %s ranges cannot be " + "expressed as a descriptor and the shader's writes would be dropped by a read-only " + "copy, so the binding is DECLINED (unaligned-%s-buffer-range)", + blockName.c_str(), static_cast(slice.offset + start), + static_cast(m_wireStorageOffsetAlignment), + isAtomicCounterBlock ? "atomic-counter" : "shader-storage", + isAtomicCounterBlock ? "atomic" : "storage"); return false; } - - const auto range = bindingPoint.GetRange(); - const VkDeviceSize bufferSize = static_cast(bufferObject->GetSize()); - VkDeviceSize rangeStart = static_cast(std::min(range.start, bufferObject->GetSize())); - VkDeviceSize rangeEnd = static_cast(std::min(range.end, bufferObject->GetSize())); - if (rangeEnd <= rangeStart) { - rangeStart = 0; - rangeEnd = bufferSize; + if (size > m_wireMaxStorageRange) { + // P7 A.3: clamp, do not refuse. The monolith arm binds whatever the GL range + // asked for and never consults maxStorageBufferRange at all (see the resident + // path below), so refusing here was the wire arm inventing a death the other + // backend does not have. A clamped binding keeps every byte the device can + // name reachable; past it the shader is out of the descriptor and + // robustBufferAccess answers zero, which beats losing the draw outright. + MGLOG_E_ONCE("ResolveStorageBufferDescriptor: block '%s' bound %llu bytes, past this device's " + "maxStorageBufferRange of %llu; the binding is clamped to the limit", + blockName.c_str(), static_cast(size), + static_cast(m_wireMaxStorageRange)); + size = m_wireMaxStorageRange; } - if (rangeEnd <= rangeStart) { - MGLOG_E_ONCE("ResolveStorageBufferDescriptor: empty SSBO range for block '%s'", - programObj.storageBlockNameByBinding[binding].c_str()); - return false; - } - - outBufferInfo.buffer = slice.buffer; - outBufferInfo.offset = slice.offset + rangeStart; - outBufferInfo.range = rangeEnd - rangeStart; + outBufferInfo = {slice.buffer, slice.offset + start, size}; + // Shader writes stay in the canonical server VkBuffer. The event marks the + // client's shadow stale; neither this path nor later acquires seed it back. + m_bufferManager->MarkWireBufferGpuWritten(range.Res, start, size); return true; } @@ -1878,252 +997,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo) const { - if (program.IsWire()) return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo); - outImageInfo = {}; - MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveStorageImageDescriptor: texture manager is null"); - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveStorageImageDescriptor: GL context is null"); - MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), - "ResolveStorageImageDescriptor: binding %u out of range", binding); - - const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; - if (baseLocation < 0) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: storage image binding %u has no uniform location", binding); - return false; - } - // Per ELEMENT, and this is where an image array differs from a storage-block array: GL - // gives every element of `uniform image2D g_image[4]` its own glUniform1i-assigned image - // unit, and the four units need not be consecutive or even ordered (the conformance case - // uses 0, 2, 4, 6). DoReflection reserves one uniform location per array element, so the - // element's location is the base plus its index - checked against the array's real - // extent so a descriptorCount that outran the reflection cannot walk onto the next - // uniform. - const Int location = baseLocation + static_cast(element); - if (!program.UniformLocationsAliasSameUniform(baseLocation, location)) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: binding %u element %u is past the end of its image array", - binding, element); - return false; - } - const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d out of range for binding %u", - imageUnit, binding); - return false; - } - - auto& imageBinding = MG_Pipe::gPipeInputs.GetImageTextureBinding(imageUnit); - if (imageBinding.Texture == nullptr) { - // Legal GL: an image unit with no texture bound makes loads return zero and discards - // stores (4.6 core 8.26). It is not a reason to lose the draw, which is what returning - // false here did - both SetupDraw and DispatchCompute skip everything on it. The - // placeholder is a 1x1 image of the target and format the shader's declaration asks - // for, so the descriptor is valid and the stores land where nobody can see them. - TextureTarget placeholderTarget = TextureTarget::Unknown; - VkFormat placeholderFormat = VK_FORMAT_UNDEFINED; - SharedPtr placeholder; - if (ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget, placeholderFormat)) { - placeholder = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat); - } - VkImageView placeholderView = VK_NULL_HANDLE; - if (placeholder != nullptr && - m_textureManager->TransitionTextureForStorageImage(commandBuffer, *placeholder)) { - // layered=true, layer=0: the placeholder's own view type IS the one the shader's - // image declaration demands, and that is exactly what the layered form asks for - // (see GetOrCreateStorageImageView, which only narrows the view type when a - // non-layered binding names a single layer). - placeholderView = - m_textureManager->GetOrCreateStorageImageView(*placeholder, 0, placeholderFormat, true, 0); - } - if (placeholderView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: image unit %d is unbound for binding %u, and no " - "placeholder descriptor could be built (target=%d format=%d)", - imageUnit, binding, static_cast(placeholderTarget), - static_cast(placeholderFormat)); - return false; - } - MGLOG_D("ResolveStorageImageDescriptor: image unit %d (binding %u) is unbound; using the placeholder " - "descriptor", imageUnit, binding); - outImageInfo.sampler = VK_NULL_HANDLE; - outImageInfo.imageView = placeholderView; - outImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - return true; - } - - const Bool ready = m_textureManager->TransitionTextureForStorageImage(commandBuffer, *imageBinding.Texture); - if (!ready) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to transition textureId=%d for image unit %d", - imageBinding.Texture->GetExternalIndex(), imageUnit); - return false; - } - auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*imageBinding.Texture); - if (resource == nullptr) { - return false; - } - - const Uint32 mipLevel = static_cast(std::max(0, imageBinding.Level)); - MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), - "ResolveStorageImageDescriptor: storage image format binding %u out of range", binding); - MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(), - "ResolveStorageImageDescriptor: storage image format policy binding %u out of range", - binding); - const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding]; - const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding]; - // The storage's own VkFormat is the wrong reference for a GL texture view: the view - // reinterprets it (GL 4.6 core table 8.21), and it is the VIEW's format the shader's - // image declaration was written against. Same correction the sampled path makes above. - const VkFormat storageImageSourceFormat = - imageBinding.Texture->IsTextureView() - ? m_textureManager->ResolveTextureViewWindow(*imageBinding.Texture, *resource).format - : resource->format; - const VkFormat viewFormat = ResolveStorageImageViewFormat( - reflectedFormat, imageBinding.Format, storageImageSourceFormat, useBindingFormat); - if (viewFormat == VK_FORMAT_UNDEFINED) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x " - "for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s", - imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(), - useBindingFormat ? "true" : "false"); - return false; - } - // glBindImageTexture named a level and a layer of the bound texture; on a GL texture - // view both are relative to the view, and the storage image is what the descriptor - // actually points at (see ToStorageMipLevel). - const Int32 storageImageLayer = - imageBinding.Layered != GL_FALSE - ? imageBinding.Layer - : static_cast(ToStorageArrayLayer(imageBinding.Texture.get(), imageBinding.Layer)); - const VkImageView view = m_textureManager->GetOrCreateStorageImageView( - *imageBinding.Texture, ToStorageMipLevel(imageBinding.Texture.get(), static_cast(mipLevel)), - viewFormat, imageBinding.Layered != GL_FALSE, storageImageLayer); - if (view == VK_NULL_HANDLE) { - MGLOG_E_ONCE("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u " - "bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s", - imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format, - static_cast(resource->format), static_cast(reflectedFormat), - static_cast(viewFormat), - useBindingFormat ? "true" : "false"); - return false; - } - outImageInfo.sampler = VK_NULL_HANDLE; - outImageInfo.imageView = view; - outImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - return outImageInfo.imageView != VK_NULL_HANDLE; - } - - SharedPtr UniformManager::GetFallbackTexture( - TextureTarget target, SamplerNumericDomain numericDomain) const { - // A multisample sampler cannot be served by the single-sampled 2D image below - its - // descriptor demands a multisample view - so it gets its own placeholder rather than no - // placeholder at all. Without one, ResolveSamplerDescriptor declined and - // BindProgramUniformBuffers dropped the WHOLE draw, which is how every - // sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a - // sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture - // unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an - // incomplete texture is undefined, not fatal, so the draw has to happen. - if (target == TextureTarget::Texture2DMultisample || - target == TextureTarget::Texture2DMultisampleArray) { - return GetFallbackMultisampleTexture(target, numericDomain); - } - if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) { - MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d", - static_cast(target)); - return nullptr; - } - - // The single-sampled fallback stays domain-agnostic: it is storage-image capable, so its - // image carries VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT and ResolveSampledImageViewFormat can - // hand an integer sampler an R8G8B8A8_UINT view of these same RGBA8 texels. A multisample - // image can never carry that bit, which is why the arm above needs one object per domain. - if (m_fallbackTexture2D == nullptr) { - auto fallbackTexture = MakeShared(kFallbackTexture2DExternalIndex); - fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8); - fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0, - {.texelSize = {1, 1, 1}, .byteSize = 4}); - // (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only - // sensible answer for a sampler with nothing bound. - static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255}; - fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0, - {kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)}); - fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true); - m_fallbackTexture2D = fallbackTexture; - } - - return m_fallbackTexture2D; - } - - SharedPtr UniformManager::GetFallbackMultisampleTexture( - TextureTarget target, SamplerNumericDomain numericDomain) const { - // ONE PLACEHOLDER PER NUMERIC DOMAIN, unlike the single-sampled fallback. - // - // A descriptor whose image format is in a different numeric class than the sampler that - // reads it needs a format-reinterpreting view, and building one needs - // VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image. A multisample image can never have it: - // SyncTextureResource computes storageImageCapable as `!isMultisampleTexture && ...`, and - // the only other source of the bit is the sRGB twin, which RGBA8 is not. So an RGBA8 - // placeholder handed to a usampler2DMS made GetOrCreateSampledImageView bail with "needs - // mutable image format", ResolveSamplerDescriptor return false, and the draw be dropped - - // the exact outcome the placeholder exists to prevent, just reached later. Matching the - // image's own format to the sampler's class instead means no reinterpreting view is - // needed at all. - const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray; - TextureInternalFormat internalFormat = TextureInternalFormat::RGBA8; - Uint32 domainSlot = 0; - switch (numericDomain) { - case SamplerNumericDomain::SignedInteger: - internalFormat = TextureInternalFormat::RGBA8I; - domainSlot = 1; - break; - case SamplerNumericDomain::UnsignedInteger: - internalFormat = TextureInternalFormat::RGBA8UI; - domainSlot = 2; - break; - case SamplerNumericDomain::Float: - case SamplerNumericDomain::Unknown: - default: - // Unknown reads as float, matching PlaceholderFormatForNumericDomain's own default: - // a shader whose sampler class could not be reflected is far likelier to be a plain - // sampler2DMS than an integer one, and a float view is the only one buildable without - // the mutable bit anyway. - break; - } - const Uint32 key = (arrayed ? kFallbackMultisampleExternalIndexCount / 2 : 0u) + domainSlot; - auto cached = m_fallbackMultisampleTextures.find(key); - if (cached != m_fallbackMultisampleTextures.end()) { - return cached->second; - } - - const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray - : TextureUploadTarget::Texture2DMultisample; - const Uint externalIndex = kFallbackMultisampleExternalIndexBase + key; - SharedPtr texture; - if (arrayed) { - texture = MakeShared(externalIndex); - } else { - texture = MakeShared(externalIndex); - } - texture->SetInternalFormat(internalFormat); - // TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with - // MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard - // VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder - // that re-created it would be worse than none. - texture->SetSamples(2); - texture->SetFixedSampleLocations(true); - // No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be - // written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike - // the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined, - // which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample - // texture that is not complete. The point of the placeholder is that the DRAW happens. - texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0}); - texture->TruncateMipmapLevels(uploadTarget, 1); - texture->MarkStorageDirty(uploadTarget, 0, false); - // Worth knowing if it ever fires: an integer multisample format can legitimately support - // no count above one on a device (framebufferIntegerColorSampleCounts is allowed to be - // VK_SAMPLE_COUNT_1_BIT), and SyncTextureResource's round-down would then hand this - // placeholder a single-sampled image, which is the samples-08726 shape the SetSamples(2) - // above exists to avoid. It already warns from there; nothing better is available - a - // one-sample integer image is still a draw, and declining is the outcome this whole - // placeholder replaced. - MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d domain=%d format=%d", - static_cast(target), static_cast(numericDomain), static_cast(internalFormat)); - return m_fallbackMultisampleTextures.emplace(key, Move(texture)).first->second; + return ResolveWireImageDescriptor(commandBuffer, program, programObj, binding, element, true, outImageInfo); } VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat, @@ -2194,393 +1068,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { return view; } - Bool UniformManager::ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, - Uint32 binding, TextureTarget& outTarget, - VkFormat& outFormat) const { - MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), - "ResolveUnboundStorageImagePlaceholder: binding %u out of range", binding); - MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), - "ResolveUnboundStorageImagePlaceholder: format binding %u out of range", binding); - outTarget = programObj.samplerTextureTargetByBinding[binding]; - // The shader's own format qualifier, exactly as the bound path prefers it over the one - // glBindImageTexture named - there is no binding here to name one. A `writeonly` image - // may carry no qualifier at all; its numeric class is then the only constraint, and the - // R32 member of that class is what carries it (see AcquireUnboundTexelBufferView). - outFormat = programObj.storageImageFormatByBinding[binding]; - if (outFormat == VK_FORMAT_UNDEFINED) { - outFormat = PlaceholderFormatForNumericDomain(programObj.samplerNumericDomainByBinding[binding]); - } - return outFormat != VK_FORMAT_UNDEFINED && PlaceholderShapeForTarget(outTarget).valid; - } - - SharedPtr UniformManager::GetUnboundStorageImageTexture( - TextureTarget target, VkFormat format) const { - const Uint64 key = (static_cast(target) << 32) | static_cast(format); - const auto cached = m_unboundStorageImageTextures.find(key); - if (cached != m_unboundStorageImageTextures.end()) { - return cached->second; - } - - const PlaceholderShape shape = PlaceholderShapeForTarget(target); - if (!shape.valid) { - // A multisample image uniform is the case with no answer here: its descriptor demands - // a multisample view, and a single-sampled 1x1 image is invalid Vulkan in that slot, - // not a degraded picture. The caller declines the binding exactly as it did before. - MGLOG_D("GetUnboundStorageImageTexture: no placeholder shape for target=%d", static_cast(target)); - return nullptr; - } - const TextureInternalFormat internalFormat = InternalFormatForVkFormat(format); - if (internalFormat == TextureInternalFormat::Unknown) { - MGLOG_E_ONCE("GetUnboundStorageImageTexture: no GL internal format matches VkFormat=%d", - static_cast(format)); - return nullptr; - } - - auto texture = MakePlaceholderTextureObject(target, kUnboundStorageImageExternalIndex); - if (texture == nullptr) { - return nullptr; - } - texture->SetInternalFormat(internalFormat); - const SizeT texelBytes = MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat); - for (Uint32 index = 0; index < shape.uploadTargetCount; ++index) { - texture->AllocateStorage(shape.uploadTargets[index], 0, - {.texelSize = {1, 1, shape.depth}, - .byteSize = texelBytes * static_cast(shape.depth)}); - // Not dirty: there is deliberately nothing to upload. The image is created and - // transitioned to GENERAL by the storage-image preparation pass like any other, and - // its contents are exactly as undefined as GL says a fetch through an unbound image - // unit is. - texture->MarkStorageDirty(shape.uploadTargets[index], 0, false); - } - m_unboundStorageImageTextures.emplace(key, texture); - MGLOG_D("GetUnboundStorageImageTexture: created placeholder target=%d format=%d", static_cast(target), - static_cast(format)); - return texture; - } - - Bool UniformManager::ResolveSampledBinding(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Uint32 binding, Uint32 element, - MG_State::GLState::ITextureObject*& outTexture, - const MG_State::GLState::SamplerObject*& outSampler) const { - // Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the - // texture and the sampler override - this runs per binding per full-path draw, - // and program-alternating draw streams take the full path on every draw. - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), "ResolveSampledBinding: GL context is null"); - MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(), - "ResolveSampledBinding: sampler location binding %u out of range", binding); - MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(), - "ResolveSampledBinding: sampler target binding %u out of range", binding); - const Int location = - ResolveDescriptorElementLocation(program, programObj.samplerUniformLocationByBinding[binding], element); - if (location < 0 && element > 0) { - return false; - } - const Int unit = ResolveSamplerUnitIndex(program, location, binding); - auto& textureUnit = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit); - const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; - MG_State::GLState::ITextureObject* texture = - textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get(); - // The sampler in effect, resolved BEFORE the completeness test below rather than after: - // GL's completeness rules are a property of (texture, sampler in effect), so the test - // cannot be asked without it. - const auto& samplerOverride = textureUnit.GetSamplerObject(); - const MG_State::GLState::SamplerObject* effectiveSampler = - samplerOverride ? samplerOverride.get() - : (texture != nullptr ? texture->GetSamplerObject().get() : nullptr); - // Undefined default texture (name 0, no image) resolves as "unbound", exactly - // like ResolveSamplerTextureRaw reports it. - if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) { - texture = nullptr; - } - // ...and so does a texture that fails the completeness rules for the filter in effect, - // because that is precisely what ResolveSamplerDescriptor does with it. The two used to - // disagree: this one asked only whether the default texture was UNDEFINED, so a default - // texture that had been given a base level but no mip chain - which is what the GL-CTS - // state reset between test cases leaves behind, and what any application that uploads to - // texture 0 has - stayed in the sampled set while the descriptor path swapped it for the - // fallback. SetupDraw then synced a texture no descriptor would use, the sync declined - // (GL calls it incomplete), and the null it returned was dereferenced one line later. - // Keeping the two predicates identical is the invariant; CollectSampledTextures exists to - // pre-sync exactly the textures the descriptors will hold. - if (MG_State::GLState::SamplesAsIncompleteTexture(texture, effectiveSampler)) { - texture = nullptr; - } - if (texture == nullptr) { - // ResolveSamplerDescriptor will substitute the fallback texture for this binding; - // include it in the sampled set so the pre-render-pass sync/transition pass covers - // its first use instead of leaving that work to happen inside an active pass. - // Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew - // a multisample arm and the two must not drift apart. - texture = GetFallbackTexture(preferredTarget, programObj.samplerNumericDomainByBinding[binding]).get(); - if (texture == nullptr) { - return false; - } - // The substitution changed the texture, so the "no override" arm of the effective - // sampler has to follow it to the fallback's own. - if (!samplerOverride) { - effectiveSampler = texture != nullptr ? texture->GetSamplerObject().get() : nullptr; - } - } - outTexture = texture; - outSampler = effectiveSampler; - return true; - } - - Bool UniformManager::CollectSampledTextures(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outTextures, - Vector* outBindingRecords) { - if (program.IsWire()) { outTextures.clear(); if (outBindingRecords) outBindingRecords->clear(); return true; } - outTextures.clear(); - if (outBindingRecords != nullptr) { - outBindingRecords->clear(); - } - // Nothing to prepare for a program the bind path is going to refuse; its declined - // binding has no uniform location to resolve a texture through either. - if (programObj.declinedDescriptors) { - return true; - } - - const Uint32 bindingCount = - std::min(m_maxBindings, static_cast(programObj.bindingKinds.size())); - for (Uint32 binding = 0; binding < bindingCount; ++binding) { - if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { - continue; - } - - // Every ELEMENT of a sampler array reaches its own texture through its own unit, - // so every element has to be in the sampled set: this walk is what gets those - // textures synced and transitioned to a sampled layout BEFORE the render pass - // opens, and a missed element would first be touched by the descriptor resolve - // inside an active pass. - const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); - for (Uint32 element = 0; element < descriptorCount; ++element) { - MG_State::GLState::ITextureObject* texture = nullptr; - const MG_State::GLState::SamplerObject* sampler = nullptr; - if (!ResolveSampledBinding(program, programObj, binding, element, texture, sampler)) { - continue; - } - if (outBindingRecords != nullptr) { - outBindingRecords->push_back({texture != nullptr ? texture->GetLifetimeId() : 0, - sampler != nullptr ? sampler->GetLifetimeId() : 0}); - } - - auto found = std::find(outTextures.begin(), outTextures.end(), texture); - if (found == outTextures.end()) { - outTextures.push_back(texture); - } - } - } - return true; - } - - Bool UniformManager::SampledBindingsUnchanged(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - const Vector& previousRecords) const { - if (program.IsWire()) return false; - // A declined program takes the full path every time and is refused there. - if (programObj.declinedDescriptors) { - return false; - } - SizeT recordIndex = 0; - // Iterate only the bindings this program declares (ascending), exactly like - // BindProgramUniformBuffers: this runs per draw whenever the texture bind - // generation moved, and walking all m_maxBindings slots to find the 1-8 real - // ones dominated it. - for (const Uint32 binding : programObj.activeBindings) { - if (binding >= m_maxBindings) { - break; // ascending, so nothing past the cap can follow - } - if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { - continue; - } - // Element-for-element, in the same order CollectSampledTextures recorded them - - // the two walks have to visit the identical descriptor sequence or the positional - // comparison below drifts. - const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); - for (Uint32 element = 0; element < descriptorCount; ++element) { - MG_State::GLState::ITextureObject* texture = nullptr; - const MG_State::GLState::SamplerObject* sampler = nullptr; - if (!ResolveSampledBinding(program, programObj, binding, element, texture, sampler)) { - continue; - } - if (recordIndex >= previousRecords.size()) { - return false; - } - const SampledBindingRecord& record = previousRecords[recordIndex++]; - if (record.textureLifetimeId != (texture != nullptr ? texture->GetLifetimeId() : 0) || - record.samplerLifetimeId != (sampler != nullptr ? sampler->GetLifetimeId() : 0)) { - return false; - } - } - } - return recordIndex == previousRecords.size(); - } - - Bool UniformManager::CollectStorageImageTextures( - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outTextures) const { - if (program.IsWire()) { outTextures.clear(); return true; } - outTextures.clear(); - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), - "CollectStorageImageTextures: GL context is null"); - // Same as the sampled walk: a declined program is refused at bind time, and its declined - // binding has no uniform location to reach an image unit through. - if (programObj.declinedDescriptors) { - return true; - } - - const Uint32 bindingCount = - std::min(m_maxBindings, static_cast(programObj.bindingKinds.size())); - for (Uint32 binding = 0; binding < bindingCount; ++binding) { - if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) { - continue; - } - if (binding >= programObj.samplerUniformLocationByBinding.size()) { - MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding); - return false; - } - - const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; - if (baseLocation < 0) { - MGLOG_E_ONCE("CollectStorageImageTextures: binding %u has no image uniform location", binding); - return false; - } - // Per ELEMENT, for the same reason the sampled walk above is: an image ARRAY is one - // binding whose elements each carry their own image unit, so each reaches its own - // texture. This walk is what puts those textures into the pre-pass sync and layout - // transition; collecting only element 0 left elements 1..N to be first touched by - // the descriptor resolve, which happens with a render pass already open. - const Uint32 descriptorCount = BindingDescriptorCount(programObj, binding); - for (Uint32 element = 0; element < descriptorCount; ++element) { - const Int location = ResolveDescriptorElementLocation(program, baseLocation, element); - if (location < 0) { - MGLOG_E_ONCE("CollectStorageImageTextures: binding %u element %u is past the end of its image array", - binding, element); - return false; - } - const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { - MGLOG_E_ONCE("CollectStorageImageTextures: image unit %d is invalid for binding %u element %u", - imageUnit, binding, element); - return false; - } - - auto* texture = MG_Pipe::gPipeInputs.GetImageTextureBinding(imageUnit).Texture.get(); - if (texture == nullptr) { - // ResolveStorageImageDescriptor will substitute the placeholder image for this - // binding; include it here for the same reason the sampled walk includes the - // fallback texture - this walk is what gets a storage image created, - // STORAGE-usage-marked and transitioned to GENERAL BEFORE the render pass - // opens, and all three of those are illegal once it has. A target with no - // placeholder shape (multisample) contributes nothing and is declined at - // resolve time exactly as it was. - TextureTarget placeholderTarget = TextureTarget::Unknown; - VkFormat placeholderFormat = VK_FORMAT_UNDEFINED; - if (!ResolveUnboundStorageImagePlaceholder(programObj, binding, placeholderTarget, - placeholderFormat)) { - continue; - } - texture = GetUnboundStorageImageTexture(placeholderTarget, placeholderFormat).get(); - if (texture == nullptr) { - continue; - } - } - if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) { - outTextures.push_back(texture); - } - } - } - return true; - } - Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, GLint imageLevel, GLenum imageAccess) { return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel; } - Bool UniformManager::CollectSamplerImageFeedback( - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outBindings) const { - if (program.IsWire()) { outBindings.clear(); return true; } - outBindings.clear(); - MOBILEGL_ASSERT(MG_Pipe::gPipeInputs.IsLive(), - "CollectSamplerImageFeedback: GL context is null"); - if (programObj.declinedDescriptors) return true; - - for (const Uint32 samplerBinding : programObj.activeBindings) { - if (samplerBinding >= m_maxBindings || - programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { - continue; - } - const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding); - for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) { - MG_State::GLState::ITextureObject* sampledTexture = nullptr; - const MG_State::GLState::SamplerObject* sampledSampler = nullptr; - if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement, - sampledTexture, sampledSampler) || - sampledTexture == nullptr || sampledSampler == nullptr || - IsPlaceholderTexture(sampledTexture)) { - // ResolveSamplerDescriptor uses a fallback in these cases, which cannot - // alias the image-unit binding of the original texture. The unbound and - // incomplete cases both arrive here AS that fallback now that - // ResolveSampledBinding applies the completeness rule itself, so the test is - // "is this one of ours" rather than a second completeness check. - continue; - } - // Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing - // direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches - // into failed dispatches; a correct snapshot for them needs a same-sample-count path. - const TextureTarget sampledTarget = sampledTexture->GetTarget(); - if (sampledTarget == TextureTarget::Texture2DMultisample || - sampledTarget == TextureTarget::Texture2DMultisampleArray) { - continue; - } - const auto& levelRange = sampledTexture->GetLevelRange(); - Bool aliasesWritableImage = false; - for (const Uint32 imageBinding : programObj.activeBindings) { - if (imageBinding >= m_maxBindings || - programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) { - continue; - } - if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false; - const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding]; - if (baseLocation < 0) return false; - const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding); - for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) { - const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement); - if (location < 0) return false; - const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); - if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { - return false; - } - const auto& image = MG_Pipe::gPipeInputs.GetImageTextureBinding(imageUnit); - // A sampler view exposes all layers of its target; equal texture plus an - // overlapping mip therefore aliases the writable image subresource. - if (image.Texture.get() == sampledTexture && - SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(), - image.Level, image.Access)) { - aliasesWritableImage = true; - break; - } - } - if (aliasesWritableImage) break; - } - if (aliasesWritableImage) { - outBindings.push_back({.samplerBinding = samplerBinding, - .samplerElement = samplerElement, - .texture = sampledTexture, - .sampler = sampledSampler, - .numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]}); - } - } - } - return true; - } - Bool UniformManager::ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex, Uint32 bindingPoint, UboBindResult& out) const { if (bindingPoint >= MG_Pipe::kMGPipeMaxBufferBindingPoints) @@ -2704,7 +1196,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); - if (program.IsWire() && binding >= programObj.uniformBlockIndexByBinding.size()) + if (binding >= programObj.uniformBlockIndexByBinding.size()) WireDescriptorFatal("uniform-buffer-reflection-binding"); Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; if (arrayElement > 0) { @@ -2726,101 +1218,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(static_cast(blockIndex) < activeUniformBlockCount, "ResolveUniformBufferPayload: uniform block index %d out of range (count=%u)", blockIndex, activeUniformBlockCount); - if (program.IsWire() && (blockIndex < 0 || static_cast(blockIndex) >= activeUniformBlockCount)) + if ((blockIndex < 0 || static_cast(blockIndex) >= activeUniformBlockCount)) WireDescriptorFatal("uniform-buffer-reflection-block"); const Uint32 frontendBinding = program.GetUniformBlockBinding(static_cast(blockIndex)); - if (program.IsWire()) - return ResolveWireUniformBufferPayload(program, static_cast(blockIndex), frontendBinding, out); - const Uint32 uniformBindingPointCount = - static_cast(MG_Pipe::gPipeInputs.GetBufferBindingPointCount(BufferTarget::Uniform)); - MOBILEGL_ASSERT(frontendBinding < uniformBindingPointCount, - "ResolveUniformBufferPayload: frontend UBO binding %u out of range for block '%s'", - frontendBinding, program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - - auto& bindingPoint = MG_Pipe::gPipeInputs.GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding); - const auto& bufferObject = bindingPoint.GetBoundObject(); - static thread_local Vector paddedUbo; - if (bufferObject == nullptr) { - // A block the program declares at a point with nothing bound - never bound, or its - // buffer was deleted while bound, which unbinds it (UnboundUniformBlockScenario, - // DeletedBoundBufferScenario). GL leaves what the shader READS undefined and raises no - // error; this used to dereference the null object. Answer it with a zeroed block of the - // reflected size, as the wire arm does (ResolveWireUniformBufferPayload). - const VkDeviceSize blockSize = - static_cast(program.GetUBOSizeAt(static_cast(blockIndex))); - paddedUbo.assign(static_cast(blockSize > 0 ? blockSize : 16), 0); - out.payload = paddedUbo.data(); - out.payloadSize = static_cast(paddedUbo.size()); - return true; - } - bufferObject->SyncPersistentMappedRange(); - - MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0, - "ResolveUniformBufferPayload: bound UBO data is empty for block '%s'", - program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - - const auto range = bindingPoint.GetRange(); - const VkDeviceSize bufferSize = static_cast(bufferObject->GetSize()); - const VkDeviceSize rangeStart = static_cast(range.start); - MOBILEGL_ASSERT(rangeStart < bufferSize, - "ResolveUniformBufferPayload: UBO range start %zu exceeds buffer size %zu for block '%s'", - static_cast(rangeStart), static_cast(bufferSize), - program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - - VkDeviceSize rangeEnd = static_cast(range.end); - if (rangeEnd > bufferSize) { - rangeEnd = bufferSize; - } - MOBILEGL_ASSERT(rangeEnd > rangeStart, - "ResolveUniformBufferPayload: invalid UBO range [%zu, %zu) for block '%s'", - static_cast(rangeStart), static_cast(rangeEnd), - program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - - const VkDeviceSize blockSize = static_cast(program.GetUBOSizeAt(static_cast(blockIndex))); - MOBILEGL_ASSERT(blockSize > 0, - "ResolveUniformBufferPayload: reflected UBO size is zero for block '%s'", - program.GetUniformBlockName(static_cast(blockIndex)).c_str()); - - const VkDeviceSize available = rangeEnd - rangeStart; - outSize = blockSize; - outData = bufferObject->MappedData() + static_cast(rangeStart); - if (available < blockSize) { - paddedUbo.assign(static_cast(blockSize), 0); - Memcpy(paddedUbo.data(), outData, static_cast(available)); - outData = paddedUbo.data(); - } - out.payload = outData; - out.payloadSize = outSize; - - // Zero-copy direct bind: for a persistent-mapped coherent app buffer whose full reflected - // block fits within the aligned bound range, point the descriptor straight at the app's - // resident VkBuffer (the same buffer the GLES backend binds) with the block range as the - // dynamic offset - no per-draw copy into a transient ring. Any gate failing keeps the - // UploadTransient payload above. AcquireResidentSlice does the persistent busy-tracking and - // (for the persistent case) hits a zero-work fast path returning the whole-buffer slice. - if (bufferObject->IsBackendPersistentMapped() && available >= blockSize && - (rangeStart % m_minDynamicOffsetAlignment) == 0) { - BufferSlice slice{}; - if (m_bufferManager->AcquireResidentSlice(BufferKind::Uniform, bufferObject, slice) && - slice.IsValid() && slice.offset == 0 && slice.size >= rangeStart + blockSize) { - out.directBindable = true; - out.buffer = slice.buffer; - out.range = blockSize; - out.dynamicOffset = rangeStart; - } - } - if (MG_Util::PipeStats::Enabled() && !out.directBindable) { - // D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host - // payload a split build would have to ship with set_shader_buffers. Espryt binds - // the frontend buffer to the driver and contributes nothing here, which is why - // the class is named for the payload and not for the call. Counted AFTER the - // zero-copy direct-bind decision: a direct bind repacks nothing, and counting it - // here reported a copy that never happened. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed, - static_cast(outSize)); - } - return true; + return ResolveWireUniformBufferPayload(program, static_cast(blockIndex), frontendBinding, out); } Bool UniformManager::CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const { @@ -3005,7 +1407,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32& outDynamicOffset) { UboBindResult ubo{}; const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, arrayElement, ubo); - if (program.IsWire() && !hasPayload) return false; + if (!hasPayload) return false; MOBILEGL_ASSERT(hasPayload && (ubo.directBindable || (ubo.payload != nullptr && ubo.payloadSize > 0)), "UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: missing UBO payload on binding %u element %u", binding, arrayElement); @@ -3096,7 +1498,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool UniformManager::PrepareWireTextureResources(const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj) { - if (!program.IsWire()) return true; if (programObj.declinedDescriptors || m_textureManager == nullptr) { MGL_WIRE_DECLINE_AT(DescriptorDeclined, "the program carries a descriptor MobileGL could not resolve"); return false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 385ba98d..fa167889 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -100,25 +100,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 textureLifetimeId = 0; Uint64 samplerLifetimeId = 0; }; - Bool CollectSampledTextures(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outTextures, - Vector* outBindingRecords = nullptr); - // Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures - // walk and reports whether every visited binding still resolves to the recorded - // (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a - // redundant glBindSampler, which always bumps it) does not prove the sampled set - // moved; this walk does, without rebuilding the set or falling off the fast path. - Bool SampledBindingsUnchanged(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - const Vector& previousRecords) const; - Bool CollectStorageImageTextures(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outTextures) const; - Bool CollectSamplerImageFeedback( - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - Vector& outBindings) const; static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, GLint imageLevel, GLenum imageAccess); // Resolve lazy texture uploads/promotions before the caller captures the @@ -147,16 +128,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, VkFormat resourceFormat, Bool useBindingFormat); - // True when the program reads at least one sampler and every one of them is bound to a - // texture whose GL level range is a single level. Such a sampler resolves to - // minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample - // and an explicit LOD 0 sample must read the same texel - which is what makes the - // ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads - // only GL state, so a texture that ends up single-level for another reason (one uploaded - // level under a wide level range) merely misses the rewrite. - static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj); - // ---- per-frame descriptor pools ---------------------------------------------------- // Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind, // setsPerFrame sets) and is never destroyed while the manager lives; every later one was @@ -290,36 +261,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 quietEpochs = 0; }; - static Bool ResolveSamplerTexture(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - SharedPtr& outTexture); - // Shared per-binding resolution for CollectSampledTextures and - // SampledBindingsUnchanged, so membership and comparison can never diverge: - // texture after the fallback substitution (may still be null when no fallback - // exists), effective sampler = unit override else the texture's own sampler. - // False = the binding is skipped (unbound with a non-2D fallback target). - // `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor. - Bool ResolveSampledBinding(const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, - MG_State::GLState::ITextureObject*& outTexture, - const MG_State::GLState::SamplerObject*& outSampler) const; - // Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures): - // the bound texture stays alive through the draw via GL binding state, so callers that - // only need the pointer skip the SharedPtr copy's atomic refcount churn. - static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); - // `numericDomain` is the sampler's class, and it matters only for the multisample arm - - // see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it. - SharedPtr GetFallbackTexture( - TextureTarget target, SamplerNumericDomain numericDomain) const; - // The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and - // no upload path: a multisample image cannot be written by a transfer, so its texels stay - // undefined - which is what GL promises for a texelFetch on an incomplete multisample - // texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's - // class outright rather than being reinterpreted at view time. - SharedPtr GetFallbackMultisampleTexture( - TextureTarget target, SamplerNumericDomain numericDomain) const; // ---- placeholders for UNBOUND image-backed descriptors ------------------------- // GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing // to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete @@ -336,16 +277,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); - // A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the - // view the descriptor gets has the view type the shader's image declaration demands. - // Null for a target with no single-sampled placeholder shape - multisample images, whose - // descriptor needs a multisample view that this cannot stand in for. - SharedPtr GetUnboundStorageImageTexture(TextureTarget target, - VkFormat format) const; - // The (target, format) pair a storage-image binding's placeholder is keyed by, resolved - // from reflection alone. False when the binding has no placeholder shape. - Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - TextureTarget& outTarget, VkFormat& outFormat) const; // `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. @@ -475,19 +406,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_descriptorSlotTrims = 0; VkTextureManager* m_textureManager = nullptr; VkSamplerManager* m_samplerManager = nullptr; - mutable SharedPtr m_fallbackTexture2D; - // Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated, - // never evicted - at most six tiny 1x1 images - and torn down with the manager. - mutable UnorderedMap> m_fallbackMultisampleTextures; - // See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily - // populated, never evicted (a program's declared formats are a fixed, tiny set) and torn - // down with the manager. The texel views are keyed by format AND by storage-vs-sampled + // See AcquireUnboundTexelBufferView. Lazily populated, never evicted (a program's declared + // formats are a fixed, tiny set) and torn down with the manager. The texel views are keyed + // by format AND by storage-vs-sampled // because the two descriptor kinds demand different format FEATURES of the device, so one // format can be usable for one and not the other. Deliberately NOT the per-frame // texelBufferViews list: those are destroyed at every frame boundary, and these must // outlive it or the placeholder would be rebuilt for every unbound binding every frame. UnorderedMap m_unboundTexelBufferViews; - mutable UnorderedMap> m_unboundStorageImageTextures; // Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps // capacity) so the descriptor-write path stops allocating on every draw. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 76836293..76efdb29 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -18,382 +18,6 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { - VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash( - const MG_State::GLState::VertexArrayObject& vao) const { - XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); - - for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) { - const auto& attr = vao.GetAttribute(i); - - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled))); - if (!attr.Enabled) { - continue; - } - - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor))); - - // The bound buffer's IDENTITY is a component of the key, and it has to be the - // buffer's never-reused lifetime id - NOT its heap address, which this used to - // hash. An address is recycled by the allocator, so a deleted-and-recreated - // buffer reproduces it; combined with a byte-identical attribute layout that - // reproduces the WHOLE content hash, and the hash is what - // TryBindResolvedVertexBindings accepts as proof that a memoised binding still - // reads the buffer it was resolved from. It did not: a destroyed buffer's GPU - // slice was bound for its successor's draw, which is how a transform-feedback - // capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous - // test's positions) instead of its own. - // Zero for client memory (no buffer), which is a distinct identity of its own. - // - // P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the - // buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed - // into every server-side content hash". The two are equally ABA-proof (the - // allocator maps one onto the other and bumps Gen only on slot REUSE); what - // changes is that the key is now the identity the SERVER will be handed once - // buffers travel as handles, instead of a number only the client can mint. - Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0; - if (attr.Buffer) { - // The SAME arm question the other four re-keyed sites ask, through the same - // helper: a site that decided for itself could silently key on the pre-handle - // identity while its neighbours keyed on the handle. - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - const MG_Pipe::MGPipeHandle handle = - m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId()); - bufferKey = static_cast(handle.Slot) | (static_cast(handle.Gen) << 32); - } - if (MagmaPipeAbaControlDefeatsIdentity()) { - // Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the - // pre-handle lifetime id and the handle's {slot, gen} are the same guard - // wearing two hats, and a control that defeated only the retired one would - // say nothing about the key P2 ships. - // - // The identity is replaced by a constant rather than by the raw - // BufferObject*, because the address is not recycled in practice and so - // never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a - // key with NO buffer identity in it looks like - the exact defect this - // hash was fixed for: "the hash is what TryBindResolvedVertexBindings - // accepts as proof that a memoised binding still reads the buffer it was - // resolved from", and with the identity gone it accepts a binding resolved - // from a different buffer. HandleRecycleScenario.AbaControl then draws a - // replacement VAO and gets its dead predecessor's vertex data. - bufferKey = 0; - } - } - XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey))); - } - - return XXH64_digest(m_hashState); - } - - VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor( - const MG_State::GLState::VertexArrayObject& vao) const { - const MG_Pipe::MGPipeHandle handle = - m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId()); - // One entry per mintable slot, grown on demand: the mint has no capacity, so neither - // does this, and no two live VAOs can share an entry however large the working set is. - // There is no probe in front of it because the mint itself is one - a one-entry memo - // hit for every acquisition after this draw's first, and a hash probe otherwise. - // - // The claim rule - the slot picks the entry, the whole handle (Gen included) decides - // whose it is - and negative control C's defeat of it are MagmaPipeArms.h's - // MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse - // (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it. - // What the control defeats HERE is the identity that SELECTS the entry: every VAO - // collapses onto one, handed back uncleared, so the replacement inherits the dead - // VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit - // suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out. - return MagmaPipeClaimSlotMemos(m_vaoMemos, handle); - } - - Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, - Uint64& outHash) const { - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - const VaoBackendMemos& memos = MemosFor(vao); - if (memos.HashConfigVersion != vao.GetConfigVersion()) return false; - outHash = memos.Hash; - return true; - } - return false; - } - - VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash( - const MG_State::GLState::VertexArrayObject& vao) const { - HashType hash = 0; - // P2 D12.5: the same memo, on the backend's side of the boundary. - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - VaoBackendMemos& memos = MemosFor(vao); - if (memos.HashConfigVersion == vao.GetConfigVersion()) { - return memos.Hash; - } - hash = ComputeHash(vao); - memos.Hash = hash; - memos.HashConfigVersion = vao.GetConfigVersion(); - return hash; - } - return hash; - } - - const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( - const MG_State::GLState::VertexArrayObject& vao) { - // P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this - // factory's slot-indexed table instead of on the frontend VAO. The eviction epoch - // survives the move and is still what stops a stale pointer being dereferenced: the - // POINTEE is a cache entry this factory can erase at a frame boundary, and moving the - // memo does not change that. - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - VaoBackendMemos& memos = MemosFor(vao); - if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr && - memos.StateEpoch == m_evictionEpoch) { - const auto* memoEntry = static_cast(memos.State); - memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter; - return *memoEntry; - } - const BackendVertexInputState& resolved = - GetOrCreateVertexInputState(vao, GetOrComputeHash(vao)); - // MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState: - // the reference is not worth holding across a call that can resize the table. - VaoBackendMemos& stamp = MemosFor(vao); - stamp.State = &resolved; - stamp.StateEpoch = m_evictionEpoch; - stamp.StateConfigVersion = vao.GetConfigVersion(); - // The AUX memo is deliberately NOT stamped here: its two words already live in - // VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no - // live reader anywhere, so the handle arm retires it rather than moving it. - return resolved; - } - // Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile- - // time true, so the handle arm above always returns. Written out rather than left to - // fall off the end so the function still has a return on every path a compiler sees. - return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao)); - } - - const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( - const MG_State::GLState::VertexArrayObject& vao, HashType hash) { - auto it = m_cache.find(hash); - if (it != m_cache.end()) { - it->second->lastUsedFrameBoundary = m_frameBoundaryCounter; - return *it->second; - } - - VertexInputStateBuilder builder; - Vector bindingBufferKeys; - Vector bindingBaseOffsets; - Vector bindingAttributeLocations; - Vector bindingUsesClientMemory; - Vector bindingConversions; - Vector bindingDivisors; - Uint32 unsupportedAttribMask = 0; - - for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { - const auto& attr = vao.GetAttribute(location); - if (!attr.Enabled) { - continue; - } - - VkFormat sourceVkFormat = - ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong); - VertexStreamConversion conversion = VertexStreamConversion::None; - // Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is - // load-bearing rather than belt-and-braces: the narrowing is only correct because the - // shader's `dvec` input is a `vec` by the time the pipeline is built, and what - // guarantees that is the flag being clear. It is clear on every backend today, and a - // program with a 64-bit float vertex input is demoted WHOLE for the same reason even - // where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag - // set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the - // module, so a float32 stream would be fed to a Float64 input. - const Bool narrowFloat64Arrays = -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the - // SERVER's own backend's - the client caps mirror is client memory (rule E). - MG_Config::Transport != MG_Config::TransportMode::Monolith - ? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr || - !MG_Remote::Server::ServerLoopInstance().Backend() - ->GetDynamicParameters() - .SupportsFloat64VertexAttributes) - : -#endif - MG_Backend::pActiveBackendObject == nullptr || - !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes; - if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) { - // No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the - // source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already - // narrowed every dvec input to a vec, so the array is narrowed to match rather - // than dropped. Mirrors what DirectGLES does for the same state. - const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size); - if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) { - sourceVkFormat = narrowedFormat; - conversion = VertexStreamConversion::Float64ToFloat32; - MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at " - "float32 precision through format=%d (size=%d long=%s)", - location, static_cast(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false"); - } - } - if (sourceVkFormat == VK_FORMAT_UNDEFINED) { - MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " - "enabled but cannot be mapped to a VkFormat", - location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); - unsupportedAttribMask |= (1u << location); - continue; - } - - VkFormat vkFormat = sourceVkFormat; - if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) { - if (IsScaledIntegerVertexFormat(vkFormat)) { - const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size); - if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) { - vkFormat = fallbackFormat; - conversion = VertexStreamConversion::ScaledIntegerToFloat32; - MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks " - "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d " - "(type=%s size=%d normalized=%s integer=%s)", - location, static_cast(sourceVkFormat), static_cast(vkFormat), - MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size, - attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false"); - } - } - - if (conversion == VertexStreamConversion::None) { - MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): " - "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists", - location, static_cast(sourceVkFormat), - MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); - unsupportedAttribMask |= (1u << location); - continue; - } - } - - const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); - if (attribByteSize == 0) { - MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is " - "enabled but cannot be sized", - location, MG_Util::ConvertDataTypeToString(attr.Type).c_str()); - unsupportedAttribMask |= (1u << location); - continue; - } - - // Verbatim, zero included. The frontend already resolved a pointer call's - // "tightly packed" stride 0 into the element size (see VertexAttribute::Stride), - // so a zero here is the binding model's stride 0 - every vertex reads the same - // element - which is exactly what a zero VkVertexInputBindingDescription::stride - // means. Substituting the element size fetched a fresh element per vertex and ran - // off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8). - // Client-memory arrays cannot reach zero: they only exist on the pointer path. - const Uint32 sourceStride = static_cast(attr.Stride); - const Bool packedAttribute = attr.Type == DataType::Int2101010Rev || - attr.Type == DataType::Uint2101010Rev; - const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type); - // For a client-memory array attr.Offset holds the raw client pointer, and the - // draw path re-uploads the data to a 16-aligned transient slice with attribute - // offset 0, so only the stride can violate Vulkan's fetch alignment there. - const Bool clientMemoryAttribute = attr.Buffer == nullptr; - if (conversion == VertexStreamConversion::None && requiredAlignment > 1 && - ((sourceStride % requiredAlignment) != 0 || - (!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) { - // GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not - // unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one - // attribute into a tightly packed transient stream without changing its format. - conversion = VertexStreamConversion::Repack; - MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment " - "(offset=%zu stride=%u required=%zu); using a tightly packed stream", - location, attr.Offset, sourceStride, requiredAlignment); - } - - Uint32 stride = sourceStride; - // A converted stream is tightly packed, so its stride is the converted element - // size - unless the source stride is zero, which does not describe a packing at - // all but "never advance". That survives the conversion unchanged: the draw path - // converts exactly one element and every vertex reads it. - if (sourceStride != 0) { - if (conversion == VertexStreamConversion::Repack) { - stride = static_cast(attribByteSize); - } else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 || - conversion == VertexStreamConversion::Float64ToFloat32) { - stride = static_cast(attr.Size * static_cast(sizeof(Float))); - } - } - const VkVertexInputRate inputRate = - (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; - - const SizeT bufferKey = reinterpret_cast(attr.Buffer.get()); - const Uint32 binding = static_cast(bindingBufferKeys.size()); - bindingBufferKeys.push_back(bufferKey); - bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0); - bindingAttributeLocations.push_back(location); - bindingUsesClientMemory.push_back(attr.Buffer == nullptr); - bindingConversions.push_back(conversion); - builder.AddBinding(binding, stride, inputRate); - builder.AddAttribute(location, binding, vkFormat, 0); - // Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything - // else needs the extension to say it. - if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) { - bindingDivisors.push_back({binding, static_cast(attr.Divisor)}); - } - } - - const auto& state = builder.Build(); - - auto& slot = m_cache[hash]; - if (!slot) { - slot = MakeUnique(); - } - BackendVertexInputState& entry = *slot; - entry.hash = hash; - entry.lastUsedFrameBoundary = m_frameBoundaryCounter; - entry.bindingDivisors = Move(bindingDivisors); - entry.bindings = builder.GetBindings(); - entry.attributes = builder.GetAttributes(); - // See the layoutHash declaration: hash only the resolved layout, never - // buffer identities, so identical layouts across VAOs/buffers agree. - XXHASH_VERIFY(XXH64_reset(m_hashState, 0)); - for (const auto& binding : entry.bindings) { - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride))); - XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate))); - } - for (const auto& attribute : entry.attributes) { - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format))); - XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset))); - } - for (const auto& divisor : entry.bindingDivisors) { - XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding))); - XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor))); - } - XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask))); - entry.layoutHash = XXH64_digest(m_hashState); - entry.attributeLocationMask = 0; - for (const auto& attribute : entry.attributes) { - if (attribute.location < 32u) { - entry.attributeLocationMask |= (1u << attribute.location); - } - } - entry.bindingBufferKeys = std::move(bindingBufferKeys); - entry.bindingBaseOffsets = std::move(bindingBaseOffsets); - entry.bindingAttributeLocations = std::move(bindingAttributeLocations); - entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory); - entry.bindingConversions = std::move(bindingConversions); - entry.unsupportedAttribMask = unsupportedAttribMask; - entry.state = state; - entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); - entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data(); - if (!entry.bindingDivisors.empty()) { - entry.divisorState.vertexBindingDivisorCount = static_cast(entry.bindingDivisors.size()); - entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data(); - entry.state.pNext = &entry.divisorState; - } else { - entry.state.pNext = nullptr; - } - return entry; - } - Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const { out = BackendVertexInputState{}; @@ -532,37 +156,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - void VertexInputStateFactory::OnFrameBoundary() { - ++m_frameBoundaryCounter; - - // Sweep occasionally; evict entries whose last hit is far in the past. - // Erasure happens only here, never mid-frame: the draw path holds a - // reference into the current entry across its setup, and unordered_map - // erase would invalidate it. Entries are CPU-side only, so no GPU-idle - // proof is needed; an evicted entry that is used again is simply rebuilt - // from the VAO state (same hash, same content). - constexpr Uint64 kSweepInterval = 256; - constexpr Uint64 kRetireAgeBoundaries = 1024; - if ((m_frameBoundaryCounter % kSweepInterval) != 0) { - return; - } - - for (auto it = m_cache.begin(); it != m_cache.end();) { - if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) { - it = m_cache.erase(it); - // Invalidate every VAO's state-pointer memo: the erased node's - // address may be reused by a future insert. Advance through the - // process-wide source so the value stays unique across factory - // instances (see the member comment). With no legacy arm the memos - // live in this factory and die with it, so a per-instance bump is - // enough - P2 D12.5. - ++m_evictionEpoch; - } else { - ++it; - } - } - } - VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra, Bool isLong) { if (isBgra) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h index 0b29ad2a..311dced2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h @@ -76,45 +76,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; }; - // The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not - // this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen} - // for the same VAO, and a table that outlived the context it was minted for would share - // one reclamation clock across two live contexts (review v2 minor 4). - VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice, - MagmaPipeIdentityTables& identity): - m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {} + VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice): + m_config(config), m_physicalDevice(physicalDevice) {} ~VertexInputStateFactory() = default; VertexInputStateFactory(const VertexInputStateFactory&) = delete; - // The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the - // entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask. - // Readers that find the aux memo valid can use these without resolving the entry. - static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) { - return (static_cast(unsupportedAttribMask) << 32) | attributeLocationMask; - } - - HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const; - // Memoized ComputeHash: reuses the VAO's cached hash while its config version - // is unchanged. Use this on per-draw paths. - HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const; - // The VAO's content hash IF it has already been memoized, without computing one. - // P2 D12.5: the three draw-path readers that used to ask the VAO object this - // question ask the factory instead, because that is where the memo lives once the - // frontend object stops carrying the backend's state. - Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const; - const BackendVertexInputState& GetOrCreateVertexInputState( - const MG_State::GLState::VertexArrayObject& vao, HashType hash); - const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao); - // Frame boundary hook: ages the cache and evicts entries not hit for many - // frames. The key mixes each bound buffer's never-reused lifetime id, so - // buffer/VAO churn keeps minting fresh keys - and does so by construction, - // not by luck: a recreated buffer can no longer land back on its dead - // predecessor's key. Without eviction the map grows for the whole session. - // Entries hold no Vulkan handles (pipeline creation copies the descriptions) - // and the draw path's entry reference never spans a frame boundary, so - // eviction here needs no GPU-idle proof. Self-gated: one counter bump and - // compare except on sweep boundaries. - void OnFrameBoundary(); static SizeT GetComponentSize(DataType type); // Tightly-packed byte size of one vertex element for this attribute: componentSize * size for // normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for @@ -133,76 +99,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { static VkFormat ToFloat32VertexFormat(Int componentCount); Bool SupportsVertexBufferFormat(VkFormat format) const; - // ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ---- - // - // The two facts that used to live as `mutable` fields on VertexArrayObject - // (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on - // the VAO's {slot, gen} and guarded by exactly the same config version. A frontend - // state object holding the backend's raw pointer is what P2 retires: under split the - // backend is in another process and its cache entry has no address a client could - // store, so the memo has to live on the side that owns the pointee. - // - // The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash - // and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree, - // so the handle arm simply stops writing it (D12.5 says delete rather than move). - struct VaoBackendMemos { - // Whose memos these are. The identity table can recycle a slot for a different - // VAO under LRU pressure, and the handle compare - Gen included - is what says - // the contents are this object's and not its predecessor's. - MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle; - Uint64 Hash = 0; - Uint32 HashConfigVersion = ~0u; - const void* State = nullptr; - Uint64 StateEpoch = 0; - Uint32 StateConfigVersion = ~0u; - }; - // Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed - // out, and NO CAPACITY: these two memos had none before this package either - they - // were unbounded mutable fields on the VertexArrayObject itself - and re-introducing - // eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an - // entry reference stays valid across the nested GetOrCreateVertexInputState call. - // 48 B per live VAO, reclaimed with the slot when the object goes idle. - mutable MagmaPipeSlotTable m_vaoMemos; - // The renderer's {slot, gen} mint (see the constructor). Never null under push. - MagmaPipeIdentityTables* m_identity = nullptr; - // The entry belonging to `vao`, claimed (and cleared) if the slot currently holds - // someone else's. - VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const; - const VulkanRendererConfig& m_config; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; // Physical-device format capabilities never change during this factory's // lifetime. The wire path rebuilds layouts without the legacy VAO memo. mutable UnorderedMap m_wireVertexFormatSupport; - // Values are heap-allocated: UnorderedMap is open-addressing, so INSERT - // invalidates references to stored values - and so does ERASE, which shifts - // the rest of the probe cluster into the hole and therefore moves entries - // other than the erased one. The draw path (and the VAOs' state-pointer - // memos) hold entry pointers across both; only the unique_ptr cell moves, - // never the pointee. - UnorderedMap> m_cache; - // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. - Uint64 m_frameBoundaryCounter = 0; - // Bumped whenever any cache entry is erased. VAOs memo a raw pointer to - // their heap-allocated entry (stable across map insert/rehash by - // construction); a memo is honored only while its recorded epoch - // matches, so an evicted entry can never be dereferenced through a - // stale memo. - // - // Drawn from a process-wide source, never a per-instance counter: the VAO - // memos outlive this factory (they live on the frontend context's VAOs, the renderer - // is destroyed and recreated on EGL surface release/re-create), so a fresh - // factory restarting at a dead factory's epoch value would honor its - // dangling entry pointers. The constructor takes a value strictly greater - // than anything a predecessor ever stamped, so a dead factory's memo can - // never compare equal here - the same never-reused idiom as the lifetime ids. - // Single-threaded like the rest of the factory (renderer-thread only). - // - // P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the - // memos live on the frontend VAOs and therefore outlive the factory. The handle arm's - // memo table is owned by this factory and dies with it, so a per-instance counter is - // enough there and the epoch shrinks back to what it looks like it should be. - Uint64 m_evictionEpoch = 1; static inline thread_local XXH64_state_t* m_hashState = XXH64_createState(); }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index fba43f95..2f500d8f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -185,19 +185,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return static_cast(target) - static_cast(TextureUploadTarget::CubeMapPositiveX); } - static Bool IsValidSampledImageLayout(VkImageLayout layout) { - switch (layout) { - case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_GENERAL: - case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: - return true; - default: - return false; - } - } - static VkImageLayout ResolveSampledReadOnlyLayout(VkImageAspectFlags aspectMask) { return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL @@ -800,16 +787,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - void VkTextureManager::BeginDrawSyncScope() { - m_drawSyncedThisDraw.clear(); - m_drawSyncScopeActive = true; - } - - void VkTextureManager::EndDrawSyncScope() { - m_drawSyncScopeActive = false; - m_drawSyncedThisDraw.clear(); - } - VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& textureOrView) { MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE"); @@ -825,23 +802,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { const TextureIdentity identity = MakeTextureIdentity(&texture); - // Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully - // synced earlier in this draw cannot have changed since (no GL mutation runs - // mid-SetupDraw), so skip the heavy SyncTexture work and hand back the - // already-synced resource. Layout lives on the resource and is updated by the - // transition path, so the short-circuited resource still reflects the truth. - const Bool memoActive = m_drawSyncScopeActive; - if (memoActive) { - for (const DrawSyncedTexture& synced : m_drawSyncedThisDraw) { - if (synced.identity == identity) { - if (synced.resource != nullptr && synced.resource->image != VK_NULL_HANDLE) { - return synced.resource; - } - break; // resource unexpectedly gone -> fall through to a full sync - } - } - } - // Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map // lookups and the (re)registration path for repeat-bound textures. TextureResource* resourcePtr = nullptr; @@ -903,19 +863,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return nullptr; } - if (memoActive) { - Bool recorded = false; - for (const DrawSyncedTexture& synced : m_drawSyncedThisDraw) { - if (synced.identity == identity) { - recorded = true; - break; - } - } - if (!recorded) { - m_drawSyncedThisDraw.push_back({identity, resourcePtr}); - } - } - return resourcePtr; } @@ -1051,52 +998,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return attachmentView; } - VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, - Uint32 mipLevel) { - TextureResource* resource = SyncTextureAndGetDescriptor(texture); - if (resource == nullptr || resource->image == VK_NULL_HANDLE) { - return VK_NULL_HANDLE; - } - // As in GetOrCreateViewAtMipLevel: perMipSampledViews belongs to the storage texture's - // own format and aspect, so a GL view has to go to the keyed cache. - if (texture.IsTextureView()) { - if (mipLevel >= resource->mipLevels) { - return VK_NULL_HANDLE; - } - TextureViewWindow window = ResolveTextureViewWindow(texture, *resource); - // Storage space already (see ToStorageMipLevel); only the level COUNT narrows. - window.baseMipLevel = mipLevel; - window.levelCount = 1; - return GetOrCreateWindowedSampledView(texture, *resource, window); - } - if (mipLevel >= resource->mipLevels) { - return VK_NULL_HANDLE; - } - - if (resource->perMipSampledViews.size() != resource->mipLevels) { - resource->perMipSampledViews.resize(resource->mipLevels, VK_NULL_HANDLE); - } - - VkImageView& perMipSampledView = resource->perMipSampledViews[mipLevel]; - if (perMipSampledView != VK_NULL_HANDLE) { - return perMipSampledView; - } - - const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); - const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); - const VkImageAspectFlags sampledAspect = - ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode()); - perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType, - mipLevel, 1, 0, resource->arrayLayers, &sampledComponents); - if (perMipSampledView == VK_NULL_HANDLE) { - MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u", __func__, texture.GetExternalIndex(), - mipLevel); - return VK_NULL_HANDLE; - } - - return perMipSampledView; - } - // 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 @@ -1230,121 +1131,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return view; } - VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, - Uint32 mipLevel, VkFormat format, - Bool layered, Int32 layer) { - // mipLevel and layer arrive in STORAGE space; ResolveStorageImageDescriptor converts - // the glBindImageTexture values with ToStorageMipLevel / ToStorageArrayLayer. - TextureResource* resource = SyncTextureAndGetDescriptor(texture); - if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels || - resource->sampleCount != VK_SAMPLE_COUNT_1_BIT || - (resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { - return VK_NULL_HANDLE; - } - - if (format == VK_FORMAT_UNDEFINED) { - format = resource->format; - } - if (!AreStorageImageViewFormatsCompatible(resource->format, format)) { - MGLOG_E_ONCE("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d", - __func__, static_cast(format), texture.GetExternalIndex(), - static_cast(resource->format)); - return VK_NULL_HANDLE; - } - if (format != resource->format && - (resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { - MGLOG_E_ONCE("%s: textureId=%d needs mutable image format=%d for storage view format=%d", - __func__, texture.GetExternalIndex(), static_cast(resource->format), - static_cast(format)); - return VK_NULL_HANDLE; - } - - // A GL texture view opens onto a WINDOW of the storage's layers; a layered image - // binding of it must not reach past that window into the parent's other layers. - Uint32 baseArrayLayer = ToStorageArrayLayer(&texture, 0); - Uint32 layerCount = texture.IsTextureView() - ? std::min(static_cast(texture.GetViewNumLayers()), - resource->arrayLayers - baseArrayLayer) - : resource->arrayLayers; - VkImageViewType viewType = resource->viewType; - if (!layered) { - switch (resource->viewType) { - case VK_IMAGE_VIEW_TYPE_1D_ARRAY: - viewType = VK_IMAGE_VIEW_TYPE_1D; - break; - case VK_IMAGE_VIEW_TYPE_2D_ARRAY: - case VK_IMAGE_VIEW_TYPE_CUBE: - case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY: - viewType = VK_IMAGE_VIEW_TYPE_2D; - break; - case VK_IMAGE_VIEW_TYPE_3D: - MGLOG_E_ONCE("%s: non-layered 3D storage views are unsupported for textureId=%d", - __func__, texture.GetExternalIndex()); - return VK_NULL_HANDLE; - default: - break; - } - - if (viewType != resource->viewType) { - if (layer < 0 || static_cast(layer) >= resource->arrayLayers) { - MGLOG_E_ONCE("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u", - __func__, layer, texture.GetExternalIndex(), resource->arrayLayers); - return VK_NULL_HANDLE; - } - baseArrayLayer = static_cast(layer); - layerCount = 1; - } - } - - const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers && - viewType == resource->viewType; - if (format == resource->format && isFullResourceView && !texture.IsTextureView()) { - return GetOrCreateViewAtMipLevel(texture, mipLevel); - } - - const TextureResource::StorageImageViewKey key{ - .mipLevel = mipLevel, - .baseArrayLayer = baseArrayLayer, - .layerCount = layerCount, - .viewType = viewType, - .format = format, - }; - auto it = resource->storageImageViews.find(key); - if (it != resource->storageImageViews.end()) { - return it->second; - } - - VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; - if (format != resource->format && - (format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) { - requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT; - } - VkFormatProperties formatProperties{}; - vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); - if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) { - MGLOG_E_ONCE("%s: storage image view format=%d lacks required features=0x%x for textureId=%d " - "(available=0x%x)", - __func__, static_cast(format), static_cast(requiredFormatFeatures), - texture.GetExternalIndex(), static_cast(formatProperties.optimalTilingFeatures)); - return VK_NULL_HANDLE; - } - - const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType, - mipLevel, 1, baseArrayLayer, layerCount, nullptr, - VK_IMAGE_USAGE_STORAGE_BIT); - if (view == VK_NULL_HANDLE) { - MGLOG_E_ONCE("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d", - __func__, texture.GetExternalIndex(), mipLevel, static_cast(resource->format), - static_cast(format)); - return VK_NULL_HANDLE; - } - resource->storageImageViews.emplace(key, view); - MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d", - __func__, texture.GetExternalIndex(), mipLevel, static_cast(resource->format), - static_cast(format)); - return view; - } - void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) { if (texture == nullptr) { return; @@ -1391,16 +1177,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) { - MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null"); - auto it = m_textureResources.find(MakeTextureIdentity(texture)); - MOBILEGL_ASSERT(it != m_textureResources.end(), - "UpdateTrackedImageLayout: textureId=%d has no tracked resource", texture->GetExternalIndex()); - MOBILEGL_ASSERT(it->second.image != VK_NULL_HANDLE, - "UpdateTrackedImageLayout: textureId=%d has null image", texture->GetExternalIndex()); - it->second.layout = newLayout; - } - void VkTextureManager::UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject* texture, Uint32 writtenMipLevel, @@ -1457,297 +1233,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.layout = newLayout; } - Bool VkTextureManager::TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture) { - TextureResource* resource = SyncTextureAndGetDescriptor(texture); - if (resource == nullptr) { - return false; - } - if (IsValidSampledImageLayout(resource->layout)) { - return true; - } - if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) { - MGLOG_W_ONCE("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling", - texture.GetExternalIndex()); - } - - VkImageLayout targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - VkAccessFlags srcAccessMask = 0; - if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { - MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED || - resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - "TransitionTextureForSampling: unsupported color layout=%d for textureId=%d", - static_cast(resource->layout), texture.GetExternalIndex()); - if (resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { - srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - } - targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - } else if ((resource->aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) { - MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED || - resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, - "TransitionTextureForSampling: unsupported depth/stencil layout=%d for textureId=%d", - static_cast(resource->layout), texture.GetExternalIndex()); - if (resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) { - srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; - srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - } - targetLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - } else { - MOBILEGL_ASSERT(false, "TransitionTextureForSampling: unsupported aspect mask=0x%x for textureId=%d", - static_cast(resource->aspect), texture.GetExternalIndex()); - } - - const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask, - s_sampledReadStagesSlot->value, srcAccessMask, - VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels); - MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex()); - // Pre-pass stream bookkeeping: a command referencing the image was recorded. - StampResourceRecordingUse(*resource); - return ok; - } - - Bool VkTextureManager::TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, - MG_State::GLState::ITextureObject& texture) { - TextureResource* resource = SyncTextureAndGetDescriptor(texture); - if (resource == nullptr) { - return false; - } - if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) { - MGLOG_D("TransitionTextureForStorageImage: multisample textureId=%d is not exposed as a storage image", - texture.GetExternalIndex()); - return false; - } - if (resource->layout == VK_IMAGE_LAYOUT_GENERAL) { - return true; - } - - VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; - VkAccessFlags srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT; - GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask); - - const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, - VK_IMAGE_LAYOUT_GENERAL, srcStageMask, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask, - VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, - resource->aspect, 0, resource->mipLevels); - MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d", - texture.GetExternalIndex()); - // Pre-pass stream bookkeeping: a command referencing the image was recorded, and an - // image unit may store to it. - StampResourceRecordingWrite(*resource); - return ok; - } - - Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer, - MG_State::GLState::ITextureObject& texture, - SamplerNumericDomain numericDomain, - VkPipelineStageFlags consumerShaderStageMask, - SampledTextureSnapshot& outSnapshot) { - outSnapshot = {}; - TextureResource* source = SyncTextureAndGetDescriptor(texture); - if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT || - source->sampledLevelCount == 0) { - return false; - } - - const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain); - if (sampledFormat == VK_FORMAT_UNDEFINED || - !AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) { - MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d", - texture.GetExternalIndex(), static_cast(sampledFormat), static_cast(source->format)); - return false; - } - if (sampledFormat != source->format && - (source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { - MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d", - texture.GetExternalIndex(), static_cast(source->format), static_cast(sampledFormat)); - return false; - } - - VkImageType imageType = VK_IMAGE_TYPE_2D; - switch (source->viewType) { - case VK_IMAGE_VIEW_TYPE_1D: - case VK_IMAGE_VIEW_TYPE_1D_ARRAY: - imageType = VK_IMAGE_TYPE_1D; - break; - case VK_IMAGE_VIEW_TYPE_3D: - imageType = VK_IMAGE_TYPE_3D; - break; - default: - break; - } - - TextureResource snapshot{}; - VkImageCreateInfo imageInfo{}; - imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.flags = source->imageCreateFlags; - imageInfo.imageType = imageType; - imageInfo.extent = {source->extent.width, source->extent.height, source->depth}; - imageInfo.mipLevels = source->mipLevels; - imageInfo.arrayLayers = source->arrayLayers; - imageInfo.format = source->format; - imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; - imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - // Keep the temporary's view-format list just as narrow as the source's sampler use. This - // has no storage-image usage, so unlike an app image binding the exact list is knowable. - Vector viewFormats; - VkImageFormatListCreateInfo formatListInfo{}; - if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { - viewFormats.push_back(source->format); - if (sampledFormat != source->format) { - viewFormats.push_back(sampledFormat); - } - formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO; - formatListInfo.viewFormatCount = static_cast(viewFormats.size()); - formatListInfo.pViewFormats = viewFormats.data(); - imageInfo.pNext = &formatListInfo; - } - - VmaAllocationCreateInfo allocationInfo{}; - allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; - allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; - const VkResult createResult = - vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr); - if (createResult != VK_SUCCESS) { - MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult, - texture.GetExternalIndex()); - return false; - } - - snapshot.extent = source->extent; - snapshot.depth = source->depth; - snapshot.arrayLayers = source->arrayLayers; - snapshot.mipLevels = source->mipLevels; - snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel; - snapshot.sampledLevelCount = source->sampledLevelCount; - snapshot.format = source->format; - snapshot.aspect = source->aspect; - snapshot.viewType = source->viewType; - snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT; - snapshot.imageCreateFlags = imageInfo.flags; - snapshot.usageFlags = imageInfo.usage; - - const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); - const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); - const VkImageAspectFlags sampledAspect = - ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode()); - snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType, - snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0, - snapshot.arrayLayers, &sampledComponents); - if (snapshot.sampledView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex()); - return false; - } - - VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - VkAccessFlags sourceAccessMask = 0; - const VkImageLayout sourceLayout = source->layout; - GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask); - if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask, - VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) || - !TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, - VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel, - snapshot.sampledLevelCount)) { - return false; - } - - Vector copyRegions; - copyRegions.reserve(snapshot.sampledLevelCount); - for (Uint32 level = snapshot.sampledBaseMipLevel; - level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) { - VkImageCopy copy{}; - copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers}; - copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers}; - copy.extent = {std::max(source->extent.width >> level, 1u), - std::max(source->extent.height >> level, 1u), - std::max(source->depth >> level, 1u)}; - copyRegions.push_back(copy); - } - EndActiveRenderPassOn(commandBuffer); - vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(copyRegions.size()), copyRegions.data()); - - if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, - ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT, - consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, - VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel, - snapshot.sampledLevelCount) || - !TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout, - VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask, - VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, - source->aspect, 0, source->mipLevels)) { - return false; - } - - StampResourceRecordingUse(*source); - outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout}; - DeferResourceRelease(Move(snapshot)); - return true; - } - - void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) { - m_storageImageTextures.insert(MakeTextureIdentity(&texture)); - } - - Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const { - const TextureIdentity identity = MakeTextureIdentity(&texture); - if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) { - return false; - } - const auto it = m_textureResources.find(identity); - // No image yet: the first sync creates it with STORAGE straight away, so there is nothing - // to preserve and nothing to order against. - return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE && - !it->second.storageUsageResolved; - } - - Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const { - const TextureIdentity identity = MakeTextureIdentity(&texture); - const auto it = m_textureResources.find(identity); - // No image yet: the first sync sizes the chain from the levels the texture already - // defines, so nothing is recreated and there is nothing to order against. - if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) { - return false; - } - const TextureResource& resource = it->second; - const IntVec3 extent = {static_cast(resource.extent.width), static_cast(resource.extent.height), - static_cast(resource.depth)}; - return resource.mipLevels < ComputeFullMipLevelCount(extent); - } - - Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const { - const TextureIdentity identity = MakeTextureIdentity(&texture); - const auto it = m_textureResources.find(identity); - if (it == m_textureResources.end()) { - return true; - } - const TextureResource& resource = it->second; - if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) { - return true; - } - // The image predates this texture's first image-unit binding, so it was created without - // STORAGE usage and has to be recreated - which is illegal inside a render pass. - if (!resource.storageUsageResolved && - m_storageImageTextures.find(identity) != m_storageImageTextures.end()) { - return true; - } - // Mirror SyncTexture's cross-draw skip condition: any version drift means the sync - // path may upload or rebuild, both of which need the render pass ended first. - const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); - const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; - return resource.syncedContentVersion != texture.GetContentVersion() || - resource.syncedShapeVersion != texture.GetShapeVersion() || - resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || - resource.syncedMipLevelCount != mipLevelCount; - } - Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, @@ -1804,12 +1289,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } SizeT VkTextureManager::PruneDeadTextures() { - // Erasing entries would dangle the raw TextureResource pointers memoized for the - // current draw; every call path (BeginFrame, and CollectGarbage at the top of a - // freshly opened draw-sync scope) runs before any memo entry is recorded. - MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(), - "PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle"); - Vector expiredTextures; expiredTextures.reserve(m_aliveObjects.size()); for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) { @@ -5069,11 +4548,4 @@ namespace MobileGL::MG_Backend::DirectVulkan { vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat); } - Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) { - if (imageFormat == viewFormat) { - return true; - } - return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) && - vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat); - } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index e5258176..e8c1ac5a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -668,25 +668,11 @@ public: VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, Uint32 baseArrayLayer, Uint32 layerCount, VkImageViewType viewType); - VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format); - VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, - VkFormat format, Bool layered, Int32 layer); - void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout); void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject* texture, Uint32 writtenMipLevel, VkImageLayout newLayout); - Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); - Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); - // Copies the complete sampler-visible mip range into a transient sampled image. The source is - // restored to its prior layout, so image-store descriptors continue to name the original image. - // The transient ownership is tied to the current frame slot and is safe through its submission. - Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, - SamplerNumericDomain numericDomain, - VkPipelineStageFlags consumerShaderStageMask, - SampledTextureSnapshot& outSnapshot); - // Recording-generation bookkeeping for the pre-pass command stream. The // generation advances every time the frame command buffer (re)begins // recording; a resource whose stamp does not match was not referenced by @@ -721,27 +707,6 @@ public: Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const; // The live resource, or null. TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView); - // Records that this texture is bound to a GL image unit, so its image must carry - // VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and - // therefore before the render pass is committed: an image that has to be upgraded is - // recreated, which is illegal inside a render pass. Sticky for the texture's lifetime - - // GL lets an image binding come and go, and re-creating the image every time it does - // would cost far more than the compression it wins back. - void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture); - // True when this texture is marked but its live image predates the mark, i.e. the next sync - // will recreate it with STORAGE usage and copy the old contents forward. Callers use this to - // submit their pending recording first, so that copy cannot read pre-flush content. - Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const; - // The same ordering question for the other recreate-and-preserve trigger: true when this - // texture's live image carries a shorter mip chain than a full one, so defining the missing - // levels recreates it and copies the old contents forward. - Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const; - // Non-mutating probe for the per-draw storage-image fast path: true when preparing this - // texture as a storage image may need work that is illegal inside a render pass (resource - // creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports - // true - a false positive merely ends the render pass, a false negative would skip a barrier. - Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const; - // `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. @@ -749,8 +714,6 @@ public: GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT); static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain); static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); - static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); - // Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`. // // The barrier covers EVERY array layer of the image, and there is deliberately no layer @@ -772,29 +735,6 @@ public: SizeT CollectGarbage(); - // Per-draw sync memo. Within a single SetupDraw the same sampled texture is - // resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and - // again inside ResolveSamplerDescriptor). No GL texture mutation can happen - // mid-SetupDraw, and layout is tracked on the TextureResource independently of - // SyncTexture, so after the first successful sync of a texture in a draw the - // heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan) - // is pure redundancy. BeginDrawSyncScope opens a window in which repeat - // SyncTextureAndGetDescriptor calls short-circuit to the already-synced - // resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard. - void BeginDrawSyncScope(); - void EndDrawSyncScope(); - - // RAII guard that opens/closes a per-draw sync memo window (see above). - class DrawSyncScope { - public: - explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); } - ~DrawSyncScope() { m_manager.EndDrawSyncScope(); } - DrawSyncScope(const DrawSyncScope&) = delete; - DrawSyncScope& operator=(const DrawSyncScope&) = delete; - private: - VkTextureManager& m_manager; - }; - private: // Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch(). Uint64 m_textureImageEpoch = 1; @@ -878,20 +818,8 @@ private: // Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn // through non-draw paths (FBO clears, readbacks) still reaches the prune. Uint32 m_gcFrameCounter = 0; - // Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of - // textures already fully synced in the current draw (small N -> flat scan). - Bool m_drawSyncScopeActive = false; // SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into. Bool m_syncingYuvTextureStorage = false; - // Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable - // across rehash in the node-based m_textureResources and stays valid for the draw (a texture - // synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture - // returns the resource without re-hashing the identity into m_textureResources. - struct DrawSyncedTexture { - TextureIdentity identity; - TextureResource* resource = nullptr; - }; - Vector m_drawSyncedThisDraw; // Cross-draw sampled-texture memo: the same few textures (atlas, lightmap) // are resolved on every draw, so cache their resource pointers and skip the // alive/resource map lookups. Node-based std::unordered_map keeps the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c88c22b8..fdca749c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -193,23 +193,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - static Bool ShouldUseTransientVertexIndexBuffer(const MG_State::GLState::BufferObject& bufferObject) { - switch (bufferObject.GetUsage()) { - case BufferUsage::StreamDraw: - case BufferUsage::StreamRead: - case BufferUsage::StreamCopy: - case BufferUsage::DynamicDraw: - case BufferUsage::DynamicRead: - case BufferUsage::DynamicCopy: - return true; - case BufferUsage::StaticDraw: - case BufferUsage::StaticRead: - case BufferUsage::StaticCopy: - default: - return false; - } - } - static VkColorComponentFlags GetSupportedColorWriteMaskForComponentCount(SizeT componentCount) { switch (componentCount) { case 1: @@ -536,35 +519,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { *g_dynamicStateShadow = {}; } - // vkCmdBindVertexBuffers, skipped when this command buffer already holds these - // buffers and offsets at binding 0. - static void ShadowedBindVertexBuffers(VkCommandBuffer commandBuffer, const VkBuffer* buffers, - const VkDeviceSize* offsets, Uint32 count) { - auto& shadow = *g_dynamicStateShadow; - Bool identical = shadow.vertexBindValid && shadow.vertexBindingCount == count && - count <= DynamicStateShadow::kMaxShadowedVertexBindings; - if (identical) { - for (Uint32 i = 0; i < count; ++i) { - if (shadow.vertexBuffers[i] != buffers[i] || shadow.vertexOffsets[i] != offsets[i]) { - identical = false; - break; - } - } - } - if (identical) { - return; - } - vkCmdBindVertexBuffers(commandBuffer, 0, count, buffers, offsets); - if (count <= DynamicStateShadow::kMaxShadowedVertexBindings) { - shadow.vertexBindValid = true; - shadow.vertexBindingCount = count; - std::copy_n(buffers, count, shadow.vertexBuffers); - std::copy_n(offsets, count, shadow.vertexOffsets); - } else { - shadow.vertexBindValid = false; - } - } - static void ShadowedSetScissor(VkCommandBuffer commandBuffer, const VkRect2D& scissor) { auto& shadow = *g_dynamicStateShadow; if (shadow.scissorValid && shadow.scissor.offset.x == scissor.offset.x && @@ -864,29 +818,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - static SizeT GetComponentCountForShaderValueType(GLenum glType) { - switch (glType) { - case GL_FLOAT: - case GL_INT: - case GL_UNSIGNED_INT: - return 1; - case GL_FLOAT_VEC2: - case GL_INT_VEC2: - case GL_UNSIGNED_INT_VEC2: - return 2; - case GL_FLOAT_VEC3: - case GL_INT_VEC3: - case GL_UNSIGNED_INT_VEC3: - return 3; - case GL_FLOAT_VEC4: - case GL_INT_VEC4: - case GL_UNSIGNED_INT_VEC4: - return 4; - default: - return 0; - } - } - static NumericDomain GetNumericDomainForVertexFormat(VkFormat format) { switch (format) { case VK_FORMAT_R32_SFLOAT: @@ -1071,163 +1002,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - template - static Float ConvertIntegerVertexComponentToFloat(ComponentT value, Bool normalized) { - if (!normalized) { - return static_cast(value); - } - if constexpr (std::is_signed_v) { - const Float scaled = static_cast(value) / - static_cast(std::numeric_limits::max()); - return std::max(-1.0f, scaled); - } else { - return static_cast(value) / - static_cast(std::numeric_limits::max()); - } - } - - template - static Bool ConvertIntegerVertexStreamToFloat32( - const MG_State::GLState::VertexAttribute& attribute, - const Uint8* sourceData, - SizeT sourceStride, - SizeT elementCount, - Vector& outData) { - if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) { - return false; - } - - const SizeT componentCount = static_cast(attribute.Size); - outData.resize(elementCount * componentCount); - for (SizeT element = 0; element < elementCount; ++element) { - const Uint8* sourceElement = sourceData + element * sourceStride; - Float* destinationElement = outData.data() + element * componentCount; - for (SizeT component = 0; component < componentCount; ++component) { - ComponentT value{}; - Memcpy(&value, sourceElement + component * sizeof(ComponentT), sizeof(ComponentT)); - destinationElement[component] = - ConvertIntegerVertexComponentToFloat(value, attribute.Normalized); - } - } - return true; - } - - static Bool ConvertScaledIntegerVertexStreamToFloat32( - const MG_State::GLState::VertexAttribute& attribute, - const Uint8* sourceData, - SizeT sourceStride, - SizeT elementCount, - Vector& outData) { - switch (attribute.Type) { - case DataType::Int8: - return ConvertIntegerVertexStreamToFloat32( - attribute, sourceData, sourceStride, elementCount, outData); - case DataType::Uint8: - return ConvertIntegerVertexStreamToFloat32( - attribute, sourceData, sourceStride, elementCount, outData); - case DataType::Int16: - return ConvertIntegerVertexStreamToFloat32( - attribute, sourceData, sourceStride, elementCount, outData); - case DataType::Uint16: - return ConvertIntegerVertexStreamToFloat32( - attribute, sourceData, sourceStride, elementCount, outData); - default: - return false; - } - } - - // The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by - // SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64 - // vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. The - // source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is deinterleaved into a - // tightly packed float32 stream rather than dropped. `normalized` is not consulted - GL - // ignores it for floating-point array types. - static Bool ConvertFloat64VertexStreamToFloat32( - const MG_State::GLState::VertexAttribute& attribute, - const Uint8* sourceData, - SizeT sourceStride, - SizeT elementCount, - Vector& outData) { - if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) { - return false; - } - - const SizeT componentCount = static_cast(attribute.Size); - outData.resize(elementCount * componentCount); - for (SizeT element = 0; element < elementCount; ++element) { - const Uint8* sourceElement = sourceData + element * sourceStride; - Float* destinationElement = outData.data() + element * componentCount; - for (SizeT component = 0; component < componentCount; ++component) { - // GL byte strides and offsets are arbitrary, so no component carries an 8-byte - // alignment guarantee; copy it out before narrowing it. - Double value = 0.0; - Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double)); - destinationElement[component] = static_cast(value); - } - } - return true; - } - - static Bool RepackVertexStream(const Uint8* sourceData, - SizeT sourceStride, - SizeT elementSize, - SizeT elementCount, - Vector& outData) { - if (sourceData == nullptr || sourceStride == 0 || elementSize == 0) { - return false; - } - outData.resize(elementCount * elementSize); - for (SizeT element = 0; element < elementCount; ++element) { - Memcpy(outData.data() + element * elementSize, - sourceData + element * sourceStride, - elementSize); - } - return true; - } - - static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { - switch (format) { - case TextureInternalFormat::R8I: - case TextureInternalFormat::R16I: - case TextureInternalFormat::R32I: - case TextureInternalFormat::RG8I: - case TextureInternalFormat::RG16I: - case TextureInternalFormat::RG32I: - case TextureInternalFormat::RGB8I: - case TextureInternalFormat::RGB16I: - case TextureInternalFormat::RGB32I: - case TextureInternalFormat::RGBA8I: - case TextureInternalFormat::RGBA16I: - case TextureInternalFormat::RGBA32I: - return NumericDomain::Sint; - case TextureInternalFormat::R8UI: - case TextureInternalFormat::R16UI: - case TextureInternalFormat::R32UI: - case TextureInternalFormat::RG8UI: - case TextureInternalFormat::RG16UI: - case TextureInternalFormat::RG32UI: - case TextureInternalFormat::RGB8UI: - case TextureInternalFormat::RGB16UI: - case TextureInternalFormat::RGB32UI: - case TextureInternalFormat::RGBA8UI: - case TextureInternalFormat::RGBA16UI: - case TextureInternalFormat::RGBA32UI: - case TextureInternalFormat::RGB10A2UI: - return NumericDomain::Uint; - case TextureInternalFormat::DepthComponent: - case TextureInternalFormat::DepthComponent16: - case TextureInternalFormat::DepthComponent24: - case TextureInternalFormat::DepthComponent32: - case TextureInternalFormat::DepthComponent32F: - case TextureInternalFormat::Depth24Stencil8: - case TextureInternalFormat::Depth32FStencil8: - case TextureInternalFormat::DepthStencil: - return NumericDomain::Unknown; - default: - return NumericDomain::FloatLike; - } - } - // Vertex attribute locations are tracked in Uint32 bitmasks, so MAX_VERTEX_ATTRIBS is both the // state-layer storage bound and the width of every mask below. Keep them in lockstep. static constexpr Uint32 kMaxVertexAttribs = @@ -1353,35 +1127,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - static const char* VkImageLayoutToString(VkImageLayout layout) { - switch (layout) { - case VK_IMAGE_LAYOUT_UNDEFINED: - return "VK_IMAGE_LAYOUT_UNDEFINED"; - case VK_IMAGE_LAYOUT_GENERAL: - return "VK_IMAGE_LAYOUT_GENERAL"; - case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: - return "VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL"; - case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: - return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL"; - case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: - return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL"; - case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - return "VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL"; - case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: - return "VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL"; - case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: - return "VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL"; - case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: - return "VK_IMAGE_LAYOUT_PRESENT_SRC_KHR"; - case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: - return "VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL"; - case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: - return "VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL"; - default: - return "VK_IMAGE_LAYOUT_OTHER"; - } - } - static Bool ActiveRenderPassUsesTexture(const ActiveRenderPassInfo& activeRenderPass, const MG_State::GLState::ITextureObject& texture) { for (const auto& trackedAttachment : activeRenderPass.trackedAttachmentLayouts) { @@ -1426,11 +1171,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { ToStorageMipLevel(stencilAttachment.GetTexture().get(), stencilAttachment.GetTextureLevel()); } - static Bool IsColorAttachment(FramebufferAttachmentType attachmentType) { - return attachmentType >= FramebufferAttachmentType::Color0 && - attachmentType <= FramebufferAttachmentType::Color31; - } - static Bool HasUnsupportedCompleteRenderbufferAttachment( const MG_State::GLState::FramebufferObject& framebufferObject) { if (framebufferObject.GetExternalIndex() == 0) { @@ -1467,190 +1207,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { "DirectVulkan does not support this non-default framebuffer configuration.")); } - static Bool IsValidSampledImageLayout(VkImageLayout layout) { - switch (layout) { - case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_GENERAL: - case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: - case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: - return true; - default: - return false; - } - } - namespace { static constexpr Uint32 kDescriptorSetsPerFrame = 64; - static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { - Int maxDimension = std::max( - baseTexelSize.x(), - std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); - Uint32 mipLevelCount = 1; - while (maxDimension > 1) { - maxDimension = std::max(maxDimension / 2, 1); - ++mipLevelCount; - } - return mipLevelCount; - } - - static IntVec3 ComputeMipTexelSize(const IntVec3& baseTexelSize, Uint32 relativeMipLevel) { - const Int width = std::max(baseTexelSize.x() >> static_cast(relativeMipLevel), 1); - const Int height = std::max(baseTexelSize.y() >> static_cast(relativeMipLevel), 1); - const Int depth = std::max(baseTexelSize.z() >> static_cast(relativeMipLevel), 1); - return {width, height, depth}; - } - - // How many components of a GL-space texel size actually halve down the mip chain. An array - // texture's LAYER count is not a dimension of the image (GL 4.6 core 8.14.3): it stays put - // all the way down, and GetMipmapTexelSize parks it in the slot after the image's own - // dimensions. This is the same split IsMipmapCompleteForFilter applies, and the two have to - // agree - allocating a chain whose layer count shrinks builds levels the completeness rule - // then rejects. Vulkan-space extents need none of this: layers live in arrayLayers there, - // so resource->depth is already 1 for every array target. - static Int MipShrinkingComponentCount(TextureTarget target) { - switch (target) { - case TextureTarget::Texture1DArray: - return 1; - case TextureTarget::Texture2DArray: - case TextureTarget::TextureCubeMapArray: - return 2; - default: - return 3; - } - } - - static IntVec3 ComputeMipTexelSizeWithFixedComponents(const IntVec3& baseTexelSize, Uint32 relativeMipLevel, - Int shrinkingComponents) { - IntVec3 size = baseTexelSize; - for (Int component = 0; component < shrinkingComponents && component < 3; ++component) { - size[component] = std::max(size[component] >> static_cast(relativeMipLevel), 1); - } - return size; - } - - // glGenerateMipmap defines levels BASE_LEVEL+1 up to the level the base image's extent and - // MAX_LEVEL admit, and leaves every other level exactly as it was (GL 4.6 core 8.17). That - // window is a property of the GL call rather than of the backend, so the caller hands in - // the frontend's own plan (MG_Impl/GLImpl/Texture/MipmapGenerationPlan.h) and this defines - // exactly those levels - neither a chain derived here nor a truncation of the tail. - // - // IMMUTABLE storage - glTexStorage*'s chain, and ANY view, which GL 4.6 core 8.18 makes - // immutable from birth - already owns every level generation can write, so none may be - // redefined here. Allocating through a view is destructive in a way the view's own extents - // cannot describe: its base extent is ONE slice of the storage's layers, so AllocateStorage - // through a two-layer view of a four-layer array resizes the storage owner's level to two - // layers and the storage behind its other two is gone. - static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture, - Uint32 baseMipLevel, Uint32 endMipLevel) { - const Uint32 existingMipLevelCount = static_cast(texture.GetMipmapLevelCount()); - if (existingMipLevelCount <= baseMipLevel) { - return false; - } - - const auto& uploadTargets = texture.GetUploadTargets(); - if (uploadTargets.empty()) { - return false; - } - - if (texture.IsImmutable()) { - return true; - } - - const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget()); - - for (const auto uploadTarget : uploadTargets) { - const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel); - const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel); - if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 || - baseByteSize == 0) { - return false; - } - - const SizeT baseTexelCount = static_cast(baseTexelSize.x()) * - static_cast(baseTexelSize.y()) * - static_cast(baseTexelSize.z()); - if (baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) { - return false; - } - - const SizeT bytesPerTexel = baseByteSize / baseTexelCount; - // AllocateStorage RESIZES the level it is handed, so only levels the chain does not - // have yet are allocated: the ones inside the window that already exist - the - // levels a mutable chain defined by hand, or a previous generate built - keep the - // shadow bytes their own upload path wrote. - const Uint32 firstMissingMipLevel = std::max(baseMipLevel + 1, existingMipLevelCount); - for (Uint32 level = firstMissingMipLevel; level < endMipLevel; ++level) { - const IntVec3 levelTexelSize = ComputeMipTexelSizeWithFixedComponents( - baseTexelSize, level - baseMipLevel, shrinkingComponents); - const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * - static_cast(levelTexelSize.y()) * - static_cast(levelTexelSize.z()); - texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); - texture.MarkStorageDirty(uploadTarget, level, false); - } - } - return true; - } - - // P5c (T5 / tx): the split arm of EnsureGenerateMipmapStorageAllocated. Under an active - // transport the apply thread may not WRITE the client's level storage - AllocateStorage - // and MarkStorageDirty on a frontend TextureObjectMipmap are §6 layer-1 surfaces - // (CONTRACT-P5C §2.3) - so the generated chain is defined on the SERVER's staged-texture - // shadow instead, keyed by this renderer's own texture twin (the TextureResource, - // node-stable in VkTextureManager's map). Same levels, same extents - derived from the - // Vulkan-space base extent, whose depth is already 1 for every array target (layers - // live in arrayLayers there, so the fixed-component split the GL-space derivation - // needs is unnecessary here; these shadow extents answer in Vulkan space, which every - // consumer of a Magma-keyed entry shares) - and every generated level is marked - // dirty-in-shadow, because its texels are generated on the GPU and no byte answer - // exists on this side. The client's chain is left stale, which §2.3 rules CORRECT: the - // two readers that could observe the staleness are both named refusals under split. - // The upload-target list still comes from the frontend object - a shape READ, the P7 - // registry's residual, not one of the writes this arm exists to remove. - static Bool EnsureGenerateMipmapShadowAllocated(const VkTextureManager::TextureResource& resource, - Uint32 baseMipLevel, - const Vector& uploadTargets) { - if (resource.mipLevels <= baseMipLevel || uploadTargets.empty()) { - return false; - } - const IntVec3 storageBaseTexelSize = {static_cast(resource.extent.width), - static_cast(resource.extent.height), - static_cast(resource.depth)}; - const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, baseMipLevel); - if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0) { - return false; - } - const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize); - auto& store = MG_Record::ServerStagedTexture(); - const Uint64 key = MG_Record::StagedTextureStore::KeyForTwinAddress(&resource); - for (const auto uploadTarget : uploadTargets) { - for (Uint32 level = baseMipLevel + 1; level < requiredMipLevelCount; ++level) { - // Bytes the shadow held for a generated level go: the declaration below moves its - // format and byte bound (NoteLevelDefined clears them); the GPU copy is newer. - // PH-4: a twin-address key never receives a staged run (no Adopt names one), - // so these generated levels are declared BYTELESS - an Unknown format gives a - // zero byte bound and skips the Tex2D device-limit rule the defaults would - // apply to a 2D-array or 3D level's depth. - store.NoteLevelDefined(key, static_cast(uploadTarget), static_cast(level), - ComputeMipTexelSize(storageBaseTexelSize, level), - static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), - static_cast(TextureInternalFormat::Unknown)); - store.MarkLevelGpuDirty(key, static_cast(uploadTarget), static_cast(level), - true); - } - } - return true; - } - - static VkImageLayout ResolveGenerateMipmapFinalLayout(VkImageAspectFlags aspectMask) { - return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 - ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL - : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - } - static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) { return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ; @@ -2077,156 +1637,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - static Bool ResolveTextureCopyDestinationBinding(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, - VkTextureManager& textureManager, BlitImageBinding& outBinding) { - auto* resource = textureManager.SyncTextureAndGetDescriptor(texture); - if (resource == nullptr) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: failed to sync destination textureId=%d", - texture.GetExternalIndex()); - return false; - } - const VkImageAspectFlags copyAspectMask = - resource->aspect & (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); - if (copyAspectMask == 0) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: destination textureId=%d uses unsupported aspect mask=0x%x", - texture.GetExternalIndex()); - return false; - } - if (mipLevel >= resource->mipLevels) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: destination textureId=%d mip=%u out of range (mips=%u)", - texture.GetExternalIndex(), mipLevel, resource->mipLevels); - return false; - } - textureManager.StampResourceRecordingWrite(*resource); - - outBinding.image = resource->image; - outBinding.trackedLayout = &resource->layout; - outBinding.aspectMask = copyAspectMask; - outBinding.extent = { - static_cast(std::max(1u, resource->extent.width >> mipLevel)), - static_cast(std::max(1u, resource->extent.height >> mipLevel))}; - outBinding.mipLevel = mipLevel; - outBinding.mipLevelCount = 1; - outBinding.baseArrayLayer = 0; - outBinding.layerCount = 1; - outBinding.label = "destination texture"; - return true; - } - - static Bool ResolveTextureCopySourceBinding(MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, - SwapchainObject& swapchainObject, - VkTextureManager& textureManager, - VkRenderPassManager& renderPassManager, - VkImageAspectFlags requiredAspectMask, - BlitImageBinding& outBinding) { - const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); - const auto attachmentType = ResolveFramebufferCopyAttachmentType(fbo, true, requiredAspectMask); - if (attachmentType == FramebufferAttachmentType::None) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: unsupported source aspect mask=0x%x", - static_cast(requiredAspectMask)); - return false; - } - - outBinding.label = "read"; - if (isDefaultFbo) { - const auto extent = swapchainObject.GetExtent(); - outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; - outBinding.mipLevel = 0; - outBinding.mipLevelCount = 1; - outBinding.baseArrayLayer = 0; - outBinding.layerCount = 1; - outBinding.trackedLayout = nullptr; - if ((requiredAspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { - outBinding.image = swapchainObject.GetImage(swapchainImageIndex); - outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - return true; - } - - const VkImageAspectFlags swapchainAspectMask = GetSwapchainDepthStencilAspectMask(swapchainObject); - if ((swapchainAspectMask & requiredAspectMask) != requiredAspectMask) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: swapchain depth image missing required aspect mask=0x%x", - static_cast(requiredAspectMask)); - return false; - } - - outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); - outBinding.aspectMask = requiredAspectMask; - return true; - } - - const auto& attachment = fbo.GetAttachment(attachmentType); - if (!attachment.IsComplete()) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer attachment %d is incomplete", - static_cast(attachmentType)); - return false; - } - if (attachment.IsRenderbuffer()) { - const auto& renderbuffer = attachment.GetRenderbuffer(); - auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); - if (rbResource == nullptr) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u is unsupported", - renderbuffer->GetExternalIndex()); - return false; - } - if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u aspect mask=0x%x " - "does not satisfy requested mask=0x%x", - renderbuffer->GetExternalIndex(), static_cast(rbResource->aspect), - static_cast(requiredAspectMask)); - return false; - } - outBinding.image = rbResource->image; - outBinding.trackedLayout = &rbResource->layout; - outBinding.aspectMask = requiredAspectMask; - outBinding.format = rbResource->format; - outBinding.sampleCount = rbResource->sampleCount; - outBinding.extent = {static_cast(rbResource->extent.width), - static_cast(rbResource->extent.height)}; - outBinding.mipLevel = 0; - outBinding.mipLevelCount = 1; - outBinding.baseArrayLayer = 0; - outBinding.layerCount = 1; - return true; - } - if (!attachment.IsTexture()) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: unsupported read framebuffer attachment type"); - return false; - } - - auto* texture = attachment.GetTexture().get(); - MOBILEGL_ASSERT(texture != nullptr, "ResolveTextureCopySourceBinding: source texture attachment is null"); - auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); - if (resource == nullptr) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: failed to sync read framebuffer textureId=%d", - texture->GetExternalIndex()); - return false; - } - if ((resource->aspect & requiredAspectMask) != requiredAspectMask) { - MGLOG_E_ONCE("CopyTexSubImage2D skipped: read framebuffer textureId=%d aspect mask=0x%x does not satisfy requested mask=0x%x", - texture->GetExternalIndex(), static_cast(resource->aspect), - static_cast(requiredAspectMask)); - return false; - } - - outBinding.image = resource->image; - outBinding.trackedLayout = &resource->layout; - outBinding.aspectMask = requiredAspectMask; - outBinding.format = resource->format; - outBinding.sampleCount = resource->sampleCount; - const auto attachmentExtent = attachment.GetSize(); - outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; - outBinding.mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); - outBinding.mipLevelCount = 1; - if (AttachmentIsDepthSlice(attachment)) { - outBinding.depthOffset = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); - outBinding.baseArrayLayer = 0; - } else { - outBinding.baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment); - } - outBinding.layerCount = 1; - return true; - } - static BlitSurfaceTransform ToBlitSurfaceTransform(VkSurfaceTransformFlagBitsKHR preTransform) { switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: @@ -2395,11 +1805,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return static_cast(value * 255.0f + 0.5f); } - static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { - const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); - return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); - } - static Int GetReadbackChannelCount(GLenum format) { switch (format) { case GL_RGB: @@ -3344,7 +2749,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); m_uniformManager->SetWireInvalidStorageImageArm(m_wireNullDescriptor); m_vertexInputStateFactory = - MakeUnique(m_config, m_physicalDevice.handle, m_pipeIdentity); + MakeUnique(m_config, m_physicalDevice.handle); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); // Prime the first frame so Render() always targets an acquired swapchain image. @@ -3380,7 +2785,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_convertedVertexStreams.clear(); MGLOG_D("VulkanRenderer initialized"); } @@ -3581,912 +2985,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_I("VulkanRenderer shut down completed"); } - // Scans the draw's index range from host-visible index bytes and returns the largest - // fetchable vertex index. Usable only when the draw's range is exactly its - // IndexBufferView (drawParams.indexRangeIsExactView). The view's byte offset is either - // an offset into the bound element-array buffer or, with no bound buffer, a raw client - // pointer. Primitive-restart sentinels are skipped so they cannot inflate the bound. - static Bool TryComputeMaxIndexFromHostBytes(const MG_State::GLState::VertexArrayObject& vao, - const IndexBufferView& indexView, Uint32& outMaxIndex) { - SizeT indexSize = 0; - switch (indexView.indexType) { - case GL_UNSIGNED_BYTE: indexSize = 1; break; - case GL_UNSIGNED_SHORT: indexSize = 2; break; - case GL_UNSIGNED_INT: indexSize = 4; break; - default: return false; - } - - const Uint8* indexBytes = nullptr; - const auto& indexBufferShared = indexView.forceClientMemory - ? SharedPtr{} - : vao.GetIndexBufferBindingSlot().GetBoundObject(); - if (indexBufferShared != nullptr) { - const SizeT bufferSize = indexBufferShared->GetSize(); - if (indexBufferShared->MappedData() == nullptr || indexView.indexByteOffset > bufferSize || - indexView.indexByteSize > bufferSize - indexView.indexByteOffset) { - return false; - } - // Recorded-but-unexecuted GPU writes (XFB capture, SSBO, storage texel - // buffer) land in the coherent mapping this scan is about to read; - // submit-and-wait first, exactly like the restart-index rewrite does. - // A no-op unless the gpu-write flag is set. - indexBufferShared->SyncGpuWrites(); - indexBufferShared->SyncPersistentMappedRange(); - indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset; - } else { - indexBytes = reinterpret_cast(indexView.indexByteOffset); - if (indexBytes == nullptr) { - return false; - } - } - - const SizeT indexCount = indexView.indexByteSize / indexSize; - // The all-ones sentinel is only a restart marker when primitive restart is enabled; - // with restart off it is a legitimate index and excluding it would truncate the - // converted stream by exactly that vertex. - const Bool primitiveRestartActive = - MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || - MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); - const Uint32 restartSentinel = indexSize == 1 ? 0xFFu : indexSize == 2 ? 0xFFFFu : 0xFFFFFFFFu; - Uint32 maxIndex = 0; - Bool sawIndex = false; - for (SizeT i = 0; i < indexCount; ++i) { - Uint32 index = 0; - switch (indexSize) { - case 1: index = indexBytes[i]; break; - case 2: index = reinterpret_cast(indexBytes)[i]; break; - default: index = reinterpret_cast(indexBytes)[i]; break; - } - if (primitiveRestartActive && index == restartSentinel) { - continue; - } - maxIndex = std::max(maxIndex, index); - sawIndex = true; - } - if (!sawIndex) { - return false; - } - outMaxIndex = maxIndex; - return true; - } - - Bool VulkanRenderer::TryBindResolvedVertexBindings( - VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, - ResolvedVertexBindings& entry, Uint64 vaoContentHash, Uint32 activeAttribMask, - Uint64 frameSerial) { - // Layout + buffer identity in two loads from data the caller already has: the - // VAO's content hash mixes every enabled attribute's format AND its bound - // buffer's address (any change bumps the config version, invalidating the hash - // memo the caller read), and the program's active-location mask fixes the - // synthetic-binding set. Together they pin bindings.size(), every base offset - // and which buffer each binding reads, so the hit path never has to resolve the - // vertex-input factory entry at all - that chase was the dominant cost of a - // VAO-cycling frame's memo hit. - if (entry.frameSerial == 0 || entry.vertexInputHash != vaoContentHash || - entry.activeAttribMask != activeAttribMask) { - return false; - } - - if (entry.frameSerial == frameSerial) { - // What is left to establish is that the buffers still hand back the slices - // recorded here, and that none of them is a host map whose shadow needs - // pushing down. An unmoved manager-wide epoch counter says both. - if (!entry.anyBufferMapped && entry.sliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) { - ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, entry.bindingCount); - return true; - } - - // Something moved somewhere; ask the buffers themselves. - const auto& attributes = vao.GetAllAttributes(); - const MG_State::GLState::BufferObject* synced = nullptr; - for (Uint32 binding = 0; binding < entry.bindingCount; ++binding) { - auto* bufferObject = attributes[entry.attributeLocations[binding]].Buffer.get(); - if (bufferObject != entry.buffers[binding]) { - return false; - } - // What the resolving path does before every acquire: a persistent map the - // backend could not adopt into coherent GPU storage mutates its shadow with - // no API call, so the write range has to be pushed down here too. It is a - // no-op for every buffer that is not such a map; when it is not, it dispatches - // a SubData that retires the epoch below, and this draw resolves in full. - if (bufferObject != synced) { - bufferObject->SyncPersistentMappedRange(); - synced = bufferObject; - } - const auto* resource = - static_cast(bufferObject->GetBackendResource().get()); - if (resource == nullptr || resource->sliceEpoch != entry.sliceEpochs[binding]) { - return false; - } - } - - ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, entry.bindingCount); - return true; - } - - // NO cross-frame trust: a memo recorded in an earlier frame declines here and - // the draw re-resolves through the full acquire path. The epoch-compare - // revalidation that used to sit here shipped visible corruption (journeymap / - // common-mods retraces, vertex anomalies on Adreno): the acquire path is the - // frame's content-sync point, and skipping it across frames trusted the - // BumpSliceEpoch inventory to cover every way a buffer's GPU copy can go stale. - // At least one path escapes it. Until that inventory is proven complete the - // hot layout memo above (same-frame) keeps the factory-chase win, and the - // first draw of each (VAO, frame) pays one full resolve. - return false; - } - - VulkanRenderer::VaoDrawMemo* VulkanRenderer::LookupVaoDrawMemo( - const MG_State::GLState::VertexArrayObject* vao) { - if (m_vaoDrawMemoTable.empty()) { - m_vaoDrawMemoTable.resize(kVaoDrawMemoSlotCount); - } - if (MagmaPipeAbaControlDefeatsIdentity()) { - // Negative control C (P2 brief D18), ahead of BOTH arms because it defeats the - // identity half of both keys at once: the legacy arm's (address, lifetime id) pair - // and the handle arm's {slot, gen}. Every VAO lands on one entry and the entry is - // handed back without an identity compare and WITHOUT being cleared - which is - // exactly what this table would do if a replacement object reproduced its dead - // predecessor's address, or reused its slot without the generation moving. - // - // Nothing else about the entry is relaxed: whether the resolved bindings it holds - // are then USED is still decided by TryBindResolvedVertexBindings' frame serial, - // content hash, active-attribute mask and slice epochs. That is what keeps the arm - // an assertion about identity rather than about the memo as a whole. - VaoDrawMemo& aliased = m_vaoDrawMemoTable[kMagmaPipeAbaControlSlotIndex]; - aliased.vaoKey = vao; - aliased.vaoLifetimeId = vao->GetLifetimeId(); - return &aliased; - } - // ---- P2 D12.4, the handle arm ---- - // - // The slot PICKS the entry, and the handle DECIDES whether the entry is this VAO's - - // the same division of labour the legacy arm below gives the address and the lifetime - // id, with two differences that are both improvements: - // - // * the slot is dense from 1, so below kVaoDrawMemoSlotCount live slots the map is a - // bijection and the two-way probe never collides at all, where an address hash - // collides by the birthday rule from the first few dozen VAOs; - // * the handle is an exact identity - Gen moves whenever a slot changes owner - so - // neither a deleted VAO's successor at the same heap address nor a VAO whose slot - // was recycled can match a predecessor's entry, even byte-identically configured. - // That is what makes the lifetime-id half of the legacy compare unnecessary here. - // - // The capacity and the victim rule are deliberately the base ref's, unchanged: this is - // the one memo of the three that HAD a capacity before P2, and an entry lost to a - // collision costs exactly what it cost then (one vertex-binding re-resolve). Above - // kVaoDrawMemoSlotCount live VAOs a set of two ways serves four slots, and degrades - // from there - never worse than the address-hashed table it replaces, which was already - // colliding. - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - const MG_Pipe::MGPipeHandle handle = ResolveVaoHandle(*vao); - const Uint32 index = MagmaPipeSlotIndex(handle) & (kVaoDrawMemoSlotCount - 1u); - VaoDrawMemo& first = m_vaoDrawMemoTable[index]; - if (first.vaoHandle == handle) { - return &first; - } - VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u]; - if (second.vaoHandle == handle) { - return &second; - } - // Miss: recycle a slot. Prefer an unclaimed one; otherwise evict the entry whose - // bindings memo is older (its VAO is the one drawn less recently). - VaoDrawMemo* victim = &first; - if (!MG_Pipe::MGPipeHandleIsNull(first.vaoHandle) && - (MG_Pipe::MGPipeHandleIsNull(second.vaoHandle) || - second.bindings.frameSerial < first.bindings.frameSerial)) { - victim = &second; - } - victim->vaoHandle = handle; - victim->vaoKey = vao; - victim->vaoLifetimeId = vao->GetLifetimeId(); - victim->contentHash = 0; - victim->layoutFactsValid = false; - // Unmatchable until a resolve completes (same rule as the legacy arm: a bailed-out - // resolve must never leave stale contents matchable). - victim->bindings.frameSerial = 0; - victim->bindings.indexFrameSerial = 0; - victim->bindings.indexBuffer = nullptr; - return victim; - } - // Multiplicative mix of the (16-byte-aligned) address; take high bits, they - // carry the most entropy of a multiply. - const Uint64 mixed = static_cast(reinterpret_cast(vao) >> 4) * 0x9E3779B97F4A7C15ull; - const Uint32 index = static_cast(mixed >> 32) & (kVaoDrawMemoSlotCount - 1); - // The address still picks the slot (it is what the caller has in hand), but it is - // the lifetime id that decides whether the slot is THIS object's: an address on - // its own is recycled, and a slot matched on a recycled address hands the new VAO - // the dead one's resolved bindings. - const Uint64 lifetimeId = vao->GetLifetimeId(); - // Negative control C has NO consumer here. It is answered once, ahead of both arms, by - // the early return above, so a run that reaches this line has the knob off and the - // lifetime-id half of the compare is unconditional. A fourth consumer here would be a - // second site deciding the same question - what MagmaPipeAbaControlDefeatsIdentity - // exists to prevent - and a trap: narrow that early return later and this one would - // silently return to D18's retired semantics. If it is ever narrowed, ask the accessor - // here rather than re-reading MG_Config::Features. - VaoDrawMemo& first = m_vaoDrawMemoTable[index]; - if (first.vaoKey == vao && first.vaoLifetimeId == lifetimeId) { - return &first; - } - VaoDrawMemo& second = m_vaoDrawMemoTable[index ^ 1u]; - if (second.vaoKey == vao && second.vaoLifetimeId == lifetimeId) { - return &second; - } - // Miss: recycle a slot. Prefer an empty one; otherwise evict the entry whose - // bindings memo is older (its VAO is the one drawn less recently). - VaoDrawMemo* victim = &first; - if (first.vaoKey != nullptr && - (second.vaoKey == nullptr || second.bindings.frameSerial < first.bindings.frameSerial)) { - victim = &second; - } - victim->vaoKey = vao; - victim->vaoLifetimeId = lifetimeId; - victim->contentHash = 0; - victim->layoutFactsValid = false; - // Unmatchable until a resolve completes (same rule as before: a bailed-out - // resolve must never leave stale contents matchable). - victim->bindings.frameSerial = 0; - victim->bindings.indexFrameSerial = 0; - victim->bindings.indexBuffer = nullptr; - return victim; - } - - Bool VulkanRenderer::UploadAndBindVertexBuffers( - VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, - const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams, - const IndexBufferView* pIndexBufferView) { - static_assert(ResolvedVertexBindings::kMaxBindings == DynamicStateShadow::kMaxShadowedVertexBindings, - "the resolved-binding memo is sized to what the bind shadow can compare"); - const Bool indexedDraw = pIndexBufferView != nullptr; - // Exclusive upper bound on the vertex-stream elements this draw can fetch through - // vertex-rate bindings, or 0 when unbounded (indirect/multi draws). Computed lazily - // because the index scan is only worth doing when a conversion actually needs it. - SizeT drawElementBound = 0; - Bool drawElementBoundComputed = false; - auto resolveDrawElementBound = [&]() -> SizeT { - if (drawElementBoundComputed) { - return drawElementBound; - } - drawElementBoundComputed = true; - if (!indexedDraw) { - drawElementBound = static_cast(drawParams.firstVertex) + drawParams.vertexCount; - } else if (drawParams.indexRangeIsExactView) { - Uint32 maxIndex = 0; - if (TryComputeMaxIndexFromHostBytes(vao, *pIndexBufferView, maxIndex)) { - drawElementBound = static_cast(maxIndex) + 1 + - static_cast(std::max(drawParams.baseVertex, 0)); - } - } - return drawElementBound; - }; - const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; - - const Uint64 frameSerial = m_bufferManager.GetFrameSerial(); - // Probe the memo BEFORE resolving the vertex-input entry: a hit needs nothing - // from it (the VAO's own hash memo pins layout and buffers - see - // TryBindResolvedVertexBindings), and skipping the resolve also skips its - // per-draw cold chase into the factory's heap entry. The direct-mapped slot - // lookup replaces the old pointer-keyed hash-map find, whose metadata and - // key-storage probing was the dominant per-draw cost of a VAO-cycling frame. - m_currentDrawResolvedEntry = nullptr; - VaoDrawMemo* slot = nullptr; - ResolvedVertexBindings* memo = nullptr; - Uint64 vaoContentHash = 0; - const Bool vaoHashKnown = VaoContentHashIfKnown(vao, vaoContentHash); - if (vaoHashKnown) { - slot = LookupVaoDrawMemo(&vao); - memo = &slot->bindings; - if (TryBindResolvedVertexBindings(commandBuffer, vao, *memo, vaoContentHash, - activeAttribMask, frameSerial)) { - m_currentDrawResolvedEntry = memo; - return true; - } - // Whatever it described is stale; a resolve that bails out below must not - // leave the old contents matchable either. - memo->frameSerial = 0; - } - auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); - if (slot == nullptr) { - // First sight since a config change: the factory resolve just stamped the - // VAO's hash memo, so the slot can be claimed (and the facts below stored) - // for every later draw of this configuration. - slot = LookupVaoDrawMemo(&vao); - memo = &slot->bindings; - memo->frameSerial = 0; - } - // Refresh the layout facts served to TrySetupDrawFastPath. Pure values derived - // from the content hash, so this is correct even for layouts whose BINDINGS are - // not memoisable (client arrays, conversions). - slot->contentHash = vertexInputState.hash; - slot->layoutHash = vertexInputState.layoutHash; - slot->layoutAuxMasks = VertexInputStateFactory::PackVertexInputAuxMasks( - vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask); - slot->layoutFactsValid = true; - const Uint32 vertexInputAttribMask = vertexInputState.attributeLocationMask; - const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; - - const auto bindingCount = vertexInputState.bindings.size() + static_cast(std::popcount(missingAttribMask)); - // Anything the memo cannot key on (see ResolvedVertexBindings) clears this as - // the resolve below discovers it. - Bool memoisable = missingAttribMask == 0 && bindingCount > 0 && - bindingCount <= ResolvedVertexBindings::kMaxBindings; - Bool anyBufferMapped = false; - - auto& vkBuffers = m_vertexBuffersScratch; - auto& vkOffsets = m_vertexOffsetsScratch; - vkBuffers.assign(bindingCount, VK_NULL_HANDLE); - vkOffsets.assign(bindingCount, 0); - - auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion, - const MG_State::GLState::VertexAttribute& attribute, - const Uint8* sourceData, SizeT sourceStride, - SizeT elementSize, SizeT elementCount, - BufferSlice& outSlice) -> Bool { - // A resolved stride of 0 is the binding model's "never advance" (see the - // factory's layout notes): exactly one element is converted and every vertex - // reads it. That single element is read at offset 0, so the stride is never - // actually used - but both converters reject 0 as a degenerate input, which - // made the documented single-element conversion unreachable and silently - // dropped every draw using such a binding. Substitute the element's own - // size; the caller's cache key still carries the distinct stride 0. - if (sourceStride == 0 && elementCount == 1) { - sourceStride = elementSize; - } - const void* uploadData = nullptr; - VkDeviceSize uploadSize = 0; - switch (conversion) { - case VertexInputStateFactory::VertexStreamConversion::Repack: - if (!RepackVertexStream(sourceData, sourceStride, elementSize, elementCount, - m_vertexRepackScratch)) { - return false; - } - uploadData = m_vertexRepackScratch.data(); - uploadSize = static_cast(m_vertexRepackScratch.size()); - break; - case VertexInputStateFactory::VertexStreamConversion::ScaledIntegerToFloat32: - if (!ConvertScaledIntegerVertexStreamToFloat32(attribute, sourceData, sourceStride, - elementCount, m_vertexConversionScratch)) { - return false; - } - uploadData = m_vertexConversionScratch.data(); - uploadSize = static_cast(m_vertexConversionScratch.size() * sizeof(Float)); - break; - case VertexInputStateFactory::VertexStreamConversion::Float64ToFloat32: - if (!ConvertFloat64VertexStreamToFloat32(attribute, sourceData, sourceStride, elementCount, - m_vertexConversionScratch)) { - return false; - } - uploadData = m_vertexConversionScratch.data(); - uploadSize = static_cast(m_vertexConversionScratch.size() * sizeof(Float)); - break; - case VertexInputStateFactory::VertexStreamConversion::None: - return false; - } - return uploadSize > 0 && - m_bufferManager.UploadTransient(BufferKind::Vertex, - m_frameContext.GetCurrentFrameIndex(), - uploadData, uploadSize, 16, outSlice); - }; - - for (SizeT binding = 0; binding < bindingCount; ++binding) { - if (binding >= vertexInputState.bindings.size()) { - break; - } - const Uint32 bindingLocation = binding < vertexInputState.bindingAttributeLocations.size() - ? vertexInputState.bindingAttributeLocations[binding] - : static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); - const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() && - vertexInputState.bindingUsesClientMemory[binding]; - const auto conversion = binding < vertexInputState.bindingConversions.size() - ? vertexInputState.bindingConversions[binding] - : VertexInputStateFactory::VertexStreamConversion::None; - if (usesClientMemory) { - memoisable = false; - const Uint32 location = bindingLocation; - MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, - "UploadAndBindVertexStreams failed to resolve client attribute location"); - - const auto& attr = vao.GetAttribute(location); - const SizeT elementSize = - VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); - const SizeT stride = attr.Stride > 0 ? static_cast(attr.Stride) : elementSize; - const auto* clientData = reinterpret_cast(attr.Offset); - if (!clientData || elementSize == 0 || stride == 0) { - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location); - return false; - } - - // Client arrays have no queryable size, so bound the upload by the draw's - // real fetch range. For indexed draws that means scanning the index bytes: - // the guessed vertexCount (indexCount + baseVertex) can both truncate draws - // whose max index exceeds their index count and over-read below it. - // AN INSTANCE-RATE BINDING IS BOUNDED BY ITS INSTANCES, not by the vertex range: - // it fetches one element per instance, shifted by the draw's baseInstance, so - // its last element is baseInstance + (instanceCount - 1) / divisor. The two - // bounds are independent and either can exceed the other (an indexed draw - // naming element 2^16, or baseInstance 1000 over a three-element array), so - // the upload covers their union. - SizeT clientElementBound = resolveDrawElementBound(); - if (vertexInputState.bindings[binding].inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && - drawParams.instanceCount > 0) { - Uint32 divisor = 1; - for (const auto& divisorEntry : vertexInputState.bindingDivisors) { - if (divisorEntry.binding == vertexInputState.bindings[binding].binding) { - divisor = divisorEntry.divisor; - break; - } - } - if (divisor > 0) { - const Uint64 first = drawParams.firstInstance; - const Uint64 last = first + (drawParams.instanceCount - 1) / divisor; - clientElementBound = - std::max(clientElementBound, static_cast(last + 1)); - } - } - BufferSlice slice{}; - Bool uploaded = false; - if (conversion == VertexInputStateFactory::VertexStreamConversion::None) { - const SizeT lastVertex = - clientElementBound > 0 - ? clientElementBound - 1 - : (drawParams.vertexCount > 0 - ? static_cast(drawParams.firstVertex) + drawParams.vertexCount - 1 - : static_cast(drawParams.firstVertex)); - const SizeT uploadSize = lastVertex * stride + elementSize; - uploaded = m_bufferManager.UploadTransient( - BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData, - static_cast(uploadSize), 16, slice); - } else { - if (clientElementBound == 0) { - // Indirect/multi indexed draws have no CPU-visible index range and a - // client array has no size to fall back to; a guessed range could - // truncate the converted stream, so skip the draw loudly. - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: converted client-memory attribute " - "location=%u has no computable vertex range", location); - return false; - } - uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize, - clientElementBound, slice); - } - if (!uploaded) { - MOBILEGL_ASSERT(false, - "UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu", - binding); - return false; - } - - vkBuffers[binding] = slice.buffer; - vkOffsets[binding] = slice.offset; - continue; - } - - // VertexInputStateFactory fills bindingBufferKeys[b] and bindingAttributeLocations[b] - // from the SAME loop iteration, one binding per enabled attribute with no merging, so - // this attribute's Buffer IS the SharedPtr by construction - no need to search the VAO's - // 32 slots for it. The client-memory branch above has already returned, so the location - // is in range here. - const auto& sourceBufferShared = vao.GetAttribute(bindingLocation).Buffer; - MOBILEGL_ASSERT(sourceBufferShared != nullptr, - "UploadAndBindVertexStreams failed to resolve source buffer"); - BufferSlice slice{}; - const SizeT sourceSize = sourceBufferShared->GetSize(); - const SizeT baseOffset = - binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0; - MOBILEGL_ASSERT(baseOffset <= sourceSize, - "UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu", - binding, baseOffset, sourceSize); - - if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { - memoisable = false; - MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, - "UploadAndBindVertexStreams failed to resolve converted attribute location"); - const auto& attr = vao.GetAttribute(bindingLocation); - const SizeT elementSize = - VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); - // Zero is a legal binding stride and means "never advance" (see - // VertexAttribute::Stride), so it is NOT folded into the element size here - - // it selects the single-element conversion below instead. - const SizeT sourceStride = static_cast(attr.Stride); - if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || - baseOffset > sourceSize || elementSize > sourceSize - baseOffset) { - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu " - "location=%u base=%zu size=%zu element=%zu stride=%zu", - binding, bindingLocation, baseOffset, sourceSize, elementSize, sourceStride); - return false; - } - - // A GPU-written source (XFB capture, SSBO, storage texel buffer) has its - // bytes produced by commands that are merely RECORDED at this point, and - // MappedData() aliases the coherent GPU memory they will write into - - // converting now would read pre-write garbage. Submit-and-wait first, - // mirroring the restart-index rewrite; a flag-test no-op otherwise. - sourceBufferShared->SyncGpuWrites(); - sourceBufferShared->SyncPersistentMappedRange(); - const SizeT availableElementCount = - sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride; - const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped(); - // Convert only what this draw can fetch instead of the whole buffer tail. - // Instance-rate bindings index by instance, not the vertex range, so they - // keep the tail. Indexed draws from cacheable buffers also keep the tail: a - // single cached whole-range conversion per frame is cheaper than a per-draw - // index scan. Persistent-mapped buffers are uncacheable and reconvert every - // draw, so for them the scan plus bounded conversion is the cheaper trade. - const Bool vertexRateBinding = - vertexInputState.bindings[binding].inputRate == VK_VERTEX_INPUT_RATE_VERTEX; - SizeT elementCount = availableElementCount; - if (vertexRateBinding && (!indexedDraw || !cacheable)) { - const SizeT elementBound = resolveDrawElementBound(); - if (elementBound > 0) { - elementCount = std::min(elementCount, elementBound); - } - } - const ConvertedVertexStreamKey cacheKey{ - .buffer = sourceBufferShared.get(), - .changeSerial = sourceBufferShared->GetChangeSerial(), - .baseOffset = baseOffset, - .sourceStride = static_cast(sourceStride), - .type = attr.Type, - .size = attr.Size, - .normalized = attr.Normalized, - .isInteger = attr.IsInteger, - .conversion = conversion, - }; - - Bool reusedCachedStream = false; - if (cacheable) { - const auto cached = m_convertedVertexStreams.find(cacheKey); - // A cached conversion covering at least this draw's range is a strict - // prefix match: converted streams are tightly packed from element 0. - if (cached != m_convertedVertexStreams.end() && - cached->second.elementCount >= elementCount) { - slice = cached->second.slice; - reusedCachedStream = true; - } - } - if (!reusedCachedStream) { - const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset; - if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride, - elementSize, elementCount, slice)) { - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: failed to convert binding=%zu location=%u", - binding, bindingLocation); - return false; - } - if (cacheable) { - m_convertedVertexStreams[cacheKey] = - ConvertedVertexStream{slice, elementCount, sourceBufferShared}; - } - } - vkBuffers[binding] = slice.buffer; - vkOffsets[binding] = slice.offset; - continue; - } - - if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { - if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { - MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); - return false; - } - } else { - if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, sourceBufferShared, slice)) { - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: failed to sync resident binding %zu", binding); - return false; - } - } - vkBuffers[binding] = slice.buffer; - vkOffsets[binding] = slice.offset + static_cast(baseOffset); - // bindingAttributeLocations carries MAX_VERTEX_ATTRIBS as its "no location" - // sentinel; GetAttribute() folds that to an empty attribute but the memo - // indexes the raw array, so such a binding is not memoisable. - memoisable = memoisable && bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; - if (memoisable) { - // memo is always non-null here: the slot was claimed (and its serial - // zeroed) before the resolve started. - memo->attributeLocations[binding] = static_cast(bindingLocation); - memo->buffers[binding] = sourceBufferShared.get(); - // Read after the acquire: it is the acquire that creates the resource - // and mints the epoch this slice belongs to. - const auto* resource = - static_cast(sourceBufferShared->GetBackendResource().get()); - memo->sliceEpochs[binding] = resource != nullptr ? resource->sliceEpoch : 0; - anyBufferMapped = anyBufferMapped || sourceBufferShared->IsMapped(); - } - } - - SizeT syntheticBinding = vertexInputState.bindings.size(); - for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { - if ((missingAttribMask & (1u << location)) == 0) { - continue; - } - - const auto glType = programObj.vertexInputTypes[location]; - const auto& currentValue = MG_Pipe::gPipeInputs.GetCurrentVertexAttribute(location); - VkFormat format = VK_FORMAT_UNDEFINED; - const void* sourceData = nullptr; - VkDeviceSize sourceSize = 0; - const Bool supported = TryGetCurrentVertexAttributeUploadPayload(currentValue, glType, format, - sourceData, sourceSize); - if (!supported) { - // SetupDraw's pre-flight should have rejected this already; never upload a null payload. - MGLOG_E_ONCE("UploadAndBindVertexStreams skipped: unsupported current generic vertex attribute type: " - "programHash=%llu location=%u type=0x%x", - static_cast(programObj.hash), location, glType); - return false; - } - - BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), - sourceData, sourceSize, 16, slice)) { - MOBILEGL_ASSERT(false, - "UploadAndBindVertexStreams skipped: failed to upload current attribute binding for location %u", - location); - return false; - } - - vkBuffers[syntheticBinding] = slice.buffer; - vkOffsets[syntheticBinding] = slice.offset; - ++syntheticBinding; - } - - if (bindingCount > 0) { - const Uint32 count = static_cast(bindingCount); - if (memoisable && memo != nullptr) { - std::copy_n(vkBuffers.data(), count, memo->vkBuffers); - std::copy_n(vkOffsets.data(), count, memo->vkOffsets); - // The factory keys entries on the VAO content hash, so this is the same - // value the hit path reads back from the VAO's own hash memo. - memo->vertexInputHash = vertexInputState.hash; - memo->activeAttribMask = activeAttribMask; - memo->bindingCount = count; - memo->anyBufferMapped = anyBufferMapped; - // Read after every acquire above, so it covers the epochs they minted. - memo->sliceEpochCounter = m_bufferManager.GetSliceEpochCounter(); - // Published last: the entry is only matchable once every field above is - // the one this completed resolve produced. - memo->frameSerial = frameSerial; - // The same draw's UploadAndBindIndexBuffer may extend this entry with the - // EBO slice memo; the pointer dies at the map's next insert (next draw). - m_currentDrawResolvedEntry = memo; - } - ShadowedBindVertexBuffers(commandBuffer, vkBuffers.data(), vkOffsets.data(), count); - } - return true; - } - namespace { - // Copies index data, replacing every occurrence of the application's arbitrary restart - // index with the fixed all-ones value of the index type - the only one Vulkan restarts - // on. An index that already equals the fixed value would then be indistinguishable from - // a restart, so it is nudged to the next-lowest value, which silently draws the wrong - // vertex. That is a real (if narrow) loss and it is reported once rather than left - // invisible; DirectGLES avoids it for 8- and 16-bit indices by widening the copy instead, - // and the same treatment here is follow-up work. - // - // The caller guarantees applicationRestartIndex fits the index type, so no truncating - // cast is needed - and none may be used: truncating turns glPrimitiveRestartIndex(0x100) - // over 8-bit indices into "restart on index 0", which shreds every primitive that - // references vertex 0. - void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType, - Uint32 applicationRestartIndex, Vector& output) { - output.resize(sizeBytes); - if (sizeBytes == 0 || source == nullptr) { - return; - } - Memcpy(output.data(), source, sizeBytes); - const auto rewrite = [&](auto* indices, auto fixedMax) { - const SizeT count = sizeBytes / sizeof(*indices); - for (SizeT i = 0; i < count; ++i) { - if (indices[i] == static_cast(applicationRestartIndex)) { - indices[i] = fixedMax; - } else if (indices[i] == fixedMax) { - MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses " - "the all-ones index %u: both cannot be spelled at this index width, so every " - "all-ones index is drawn one vertex lower. Use " - "GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones value out of the " - "index data.", - applicationRestartIndex, static_cast(fixedMax)); - indices[i] = fixedMax - 1; - } - } - }; - switch (indexType) { - case VK_INDEX_TYPE_UINT8: - rewrite(reinterpret_cast(output.data()), static_cast(0xFFu)); - break; - case VK_INDEX_TYPE_UINT16: - rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFu)); - break; - case VK_INDEX_TYPE_UINT32: - rewrite(reinterpret_cast(output.data()), static_cast(0xFFFFFFFFu)); - break; - default: - break; - } - } } // namespace - Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame, - const MG_State::GLState::VertexArrayObject& vao, - const IndexBufferView* pIndexBufferView) { - VkIndexType vkIndexType = VK_INDEX_TYPE_MAX_ENUM; - switch (pIndexBufferView->indexType) { - case GL_UNSIGNED_BYTE: - MOBILEGL_ASSERT(m_indexTypeUint8ExtensionEnabled, - "DrawElements with GL_UNSIGNED_BYTE requires VK_KHR_index_type_uint8 or VK_EXT_index_type_uint8"); - vkIndexType = VK_INDEX_TYPE_UINT8; - break; - case GL_UNSIGNED_SHORT: - vkIndexType = VK_INDEX_TYPE_UINT16; - break; - case GL_UNSIGNED_INT: - vkIndexType = VK_INDEX_TYPE_UINT32; - break; - default: - MGLOG_D("DrawElements skipped: index type %u is not supported yet", pIndexBufferView->indexType); - return false; - } - - // GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan - // only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX - // already matches that, so only the arbitrary form needs handling: rewrite the indices into a - // transient copy where the application's restart index becomes the fixed one. - Uint32 substituteRestartIndex = 0; - Bool substituteRestart = false; - // One bulk parameters fetch instead of up to three accessor calls per indexed - // draw; all three inputs are pure reads of these fields. - const RenderStateParameters& rsp = MG_Pipe::gPipeInputs.GetRenderStateParameters(); - if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) { - const Uint32 restartIndex = rsp.PrimitiveRestartIndex; - const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType); - // STRICTLY less, and never truncated. Equal needs no rewrite (the driver already - // restarts there); GREATER means the index type cannot hold the application's restart - // index, so GL 4.6 core 10.3.6 says nothing matches it and the draw restarts nowhere - - // which is exactly what ResolvePrimitiveRestartEnable told the pipeline, so rewriting - // here would put restarts into a stream the pipeline was built not to restart on. - substituteRestart = restartIndex < fixedMax; - substituteRestartIndex = restartIndex; - } - - // Bound by reference so the SharedPtr below is the one already in hand rather than a fresh - // GL-name map lookup plus an atomic refcount pair on every indexed draw - the vertex path - // above documents the same cost. - const SharedPtr& indexBufferShared = - vao.GetIndexBufferBindingSlot().GetBoundObject(); - const auto* indexBuffer = pIndexBufferView->forceClientMemory ? nullptr : indexBufferShared.get(); - if (indexBuffer == nullptr) { - // No element-array buffer: the view's byte offset is a raw client pointer - // (desktop drivers accept client-memory indices and the GL CTS relies on - // this even in core contexts). Snapshot the data into a transient slice. - const auto* clientIndices = reinterpret_cast(pIndexBufferView->indexByteOffset); - if (clientIndices == nullptr || pIndexBufferView->indexByteSize == 0) { - MGLOG_E_ONCE("DrawElements skipped: no element array buffer bound and no client index data"); - return false; - } - Vector rewrittenIndices; - const void* uploadSource = clientIndices; - if (substituteRestart) { - RewriteRestartIndices(clientIndices, pIndexBufferView->indexByteSize, vkIndexType, - substituteRestartIndex, rewrittenIndices); - uploadSource = rewrittenIndices.data(); - } - BufferSlice slice{}; - if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - uploadSource, pIndexBufferView->indexByteSize, 4, slice)) { - MGLOG_E_ONCE("DrawElements skipped: failed to upload client index data"); - return false; - } - auto& shadow = *g_dynamicStateShadow; - if (!shadow.indexBindValid || shadow.indexBuffer != slice.buffer || - shadow.indexOffset != slice.offset || shadow.indexType != vkIndexType) { - vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset, vkIndexType); - shadow.indexBindValid = true; - shadow.indexBuffer = slice.buffer; - shadow.indexOffset = slice.offset; - shadow.indexType = vkIndexType; - } - return true; - } - const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; - MOBILEGL_ASSERT(pIndexBufferView->indexByteOffset + indexDataSizeBytes <= indexBuffer->GetSize(), - "DrawElements index range out of bounds"); - - // EBO slice memo (see ResolvedVertexBindings): skips the per-draw - // AcquireResidentSlice when the live bound EBO and its resource epoch still - // match what the recording draw resolved. Restart substitution re-uploads per - // draw and never stores a memo, so a hit requires it off. The index TYPE is not - // memo state: it flows from the draw's view into the shadowed bind below. - ResolvedVertexBindings* indexMemo = m_currentDrawResolvedEntry; - if (indexMemo != nullptr && !substituteRestart && indexMemo->indexFrameSerial != 0 && - indexMemo->indexBuffer == indexBuffer) { - // One-compare rescue first (mirrors TryBindResolvedVertexBindings): the - // use-serial was stamped this frame and the manager-wide slice-epoch - // counter has not moved, so no buffer anywhere - this EBO included - - // changed its slice or gained a host map since the epoch was verified. - // Skips the per-draw GetBackendResource chase into a cold resource object. - Bool sliceStillValid = false; - const Uint64 frameSerial = m_bufferManager.GetFrameSerial(); - if (indexMemo->indexFrameSerial == frameSerial && !indexMemo->indexBufferMapped && - indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) { - sliceStillValid = true; - } - // NO cross-frame trust for the EBO either (same corruption class as the - // vertex half, see TryBindResolvedVertexBindings): a memo from an earlier - // frame declines and the draw re-runs the acquire, which is the sync point. - if (sliceStillValid) { - const VkDeviceSize memoBindOffset = indexMemo->indexSliceOffset + - static_cast(pIndexBufferView->indexByteOffset); - auto& shadow = *g_dynamicStateShadow; - if (!shadow.indexBindValid || shadow.indexBuffer != indexMemo->indexVkBuffer || - shadow.indexOffset != memoBindOffset || shadow.indexType != vkIndexType) { - vkCmdBindIndexBuffer(frame.commandBuffer, indexMemo->indexVkBuffer, memoBindOffset, - vkIndexType); - shadow.indexBindValid = true; - shadow.indexBuffer = indexMemo->indexVkBuffer; - shadow.indexOffset = memoBindOffset; - shadow.indexType = vkIndexType; - } - return true; - } - } - - BufferSlice slice{}; - MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); - if (substituteRestart) { - // The whole buffer is rewritten, not just this draw's range, so that every element - // index keeps its position: an indirect draw's firstIndex lives in GPU memory and - // cannot be adjusted from here. - indexBufferShared->SyncGpuWrites(); - Vector rewrittenIndices; - RewriteRestartIndices(indexBufferShared->MappedData(), indexBufferShared->GetSize(), vkIndexType, - substituteRestartIndex, rewrittenIndices); - if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), - rewrittenIndices.data(), rewrittenIndices.size(), 4, slice)) { - MGLOG_E_ONCE("DrawElements skipped: failed to upload restart-substituted index data"); - return false; - } - } else if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) { - MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data"); - if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) { - MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); - return false; - } - } else if (!m_bufferManager.AcquireResidentSlice(BufferKind::Index, indexBufferShared, slice)) { - MGLOG_E_ONCE("DrawElements skipped: failed to sync resident index buffer"); - return false; - } else if (indexMemo != nullptr && !substituteRestart) { - // Resident acquire succeeded: record the slice for the next draw of this VAO. - // Read the epoch AFTER the acquire - it is the acquire that mints the epoch - // this slice belongs to. - const auto* resource = static_cast( - indexBufferShared->GetBackendResource().get()); - if (resource != nullptr) { - indexMemo->indexBuffer = indexBuffer; - indexMemo->indexSliceEpoch = resource->sliceEpoch; - // Read after the acquire for the same reason as the epoch: the acquire - // may have bumped the manager-wide counter minting this very epoch. - indexMemo->indexSliceEpochCounter = m_bufferManager.GetSliceEpochCounter(); - indexMemo->indexVkBuffer = slice.buffer; - indexMemo->indexSliceOffset = slice.offset; - indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial(); - // A host-mapped EBO can mutate its shadow with no epoch bump; the hit - // path declines on this flag (mirror of anyBufferMapped). - indexMemo->indexBufferMapped = indexBufferShared->IsMapped(); - } - } - const VkDeviceSize indexBindOffset = - slice.offset + static_cast(pIndexBufferView->indexByteOffset); - auto& shadow = *g_dynamicStateShadow; - if (!shadow.indexBindValid || shadow.indexBuffer != slice.buffer || - shadow.indexOffset != indexBindOffset || shadow.indexType != vkIndexType) { - vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, indexBindOffset, vkIndexType); - shadow.indexBindValid = true; - shadow.indexBuffer = slice.buffer; - shadow.indexOffset = indexBindOffset; - shadow.indexType = vkIndexType; - } - return true; - } - Bool VulkanRenderer::InitializeBlitResources() { // P5f fv gave this an early return for every non-monolith transport, because the hidden // programs are frontend objects and creating them on the server would retain a client @@ -4586,67 +3087,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } - Bool VulkanRenderer::GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, - MG_State::GLState::ITextureObject& texture, - VkTextureManager::TextureResource& resource, - Uint32 baseMipLevel, - Uint32 generateMipLevelCount, - const IntVec3& storageBaseTexelSize, - VkImageLayout originalLayout, - VkImageLayout finalLayout) { - // P7 wave 2-B2 (CONTRACT-P7 §5.2, (B')): THE MONOLITH ARM ON THE BAKED PASS. Every - // level below is WireDepthMipmap.inc's GenerateWireDepthMipLevel - the same 2x2 - // texelFetch box, the same clamp, the same average, the same half-texel offset, since - // that shader is a PORT of the one this function used to compile at startup. What goes - // away with the GL program is the four frontend objects and the compiler call; what the - // texture ends up holding is the same chain. - // - // `texture`, `storageBaseTexelSize` and `originalLayout` are the pull build's; the pass - // reads its shape from `resource`, whose tracked layout IS `originalLayout` at entry. - (void)frame; - (void)texture; - (void)storageBaseTexelSize; - (void)originalLayout; - if (generateMipLevelCount <= 1) return true; - if (resource.aspect != VK_IMAGE_ASPECT_DEPTH_BIT) return false; - for (Uint32 level = baseMipLevel + 1; level < baseMipLevel + generateMipLevelCount; ++level) { - WireImage source{}; - source.image = resource.image; - source.format = resource.format; - source.aspect = VK_IMAGE_ASPECT_DEPTH_BIT; - source.layout = resource.layout; - source.trackedLayout = &resource.layout; - source.levels = resource.mipLevels; - source.level = level - 1; - source.extent = {std::max(resource.extent.width >> (level - 1), 1u), - std::max(resource.extent.height >> (level - 1), 1u)}; - WireImage destination = source; - destination.level = level; - destination.extent = {std::max(resource.extent.width >> level, 1u), - std::max(resource.extent.height >> level, 1u)}; - if (!GenerateWireDepthMipLevel(source, destination)) return false; - } - // The caller's contract is that the image is left in `finalLayout`; the per-level pass - // leaves it in the GENERAL it uses for a same-image source and destination. - WireImage whole{}; - whole.image = resource.image; - whole.format = resource.format; - whole.aspect = resource.aspect; - whole.layout = resource.layout; - whole.trackedLayout = &resource.layout; - whole.levels = resource.mipLevels; - whole.extent = resource.extent; - TransitionWireImage(whole, finalLayout); - return true; - } - - // Boost-style hash combine. The inputs are tiny enum ordinals and bit masks, so - // full avalanche is unnecessary; the combine only has to keep distinct state - // vectors apart under the memo's otherwise-exact key. - static inline Uint64 CombinePipelineStateWord(Uint64 hash, Uint64 word) { - return hash ^ (word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2)); - } - // Value hash over every fixed-function GL state the pipeline payload reads that // the memo key's other fields (mode, program hash, vertex-input hash, render-pass // hash, transform flags) do not already pin down. Enumerated against the payload @@ -4756,18 +3196,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return milliseconds; } - VkPipeline VulkanRenderer::GetOrCreatePipeline( - GLenum mode, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - ProgramFactory::CompileOptionFlags transformFlags, - const MG_State::GLState::VertexArrayObject& vao, - const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable) { - return GetOrCreatePipelineWithInput(mode, program, programObj, transformFlags, - m_vertexInputStateFactory->GetOrCreateVertexInputState(vao), renderPassEntry, primitiveRestartEnable); - } - VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, @@ -5528,178 +3956,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return pipeline; } - Bool VulkanRenderer::PrepareStorageImageTextures( - FrameContext::FrameData& frame, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj) { - if (!programObj.hasStorageImages) { - return true; - } - auto& storageTextures = m_storageImageTexturesScratch; - if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) { - MGLOG_E_ONCE("%s: failed to collect storage images for program=%u", - __func__, program.GetExternalIndex()); - return false; - } - if (storageTextures.empty()) { - return true; - } - - // Steady-state fast path: when every collected texture is already resident in GENERAL - // with no pending clear and no dirty content, the loop below has nothing to record, so - // keep the render pass alive instead of splitting it on every storage-image draw (on - // tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store - // coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here. - // Record every image-unit binding before probing anything: a texture whose image was - // created without STORAGE usage (the default - it costs UBWC compression on Adreno) - // needs a recreate, and the probe below is what ends the render pass so that recreate - // lands here rather than mid-pass. This cannot be folded into the probe loop, which - // stops at the first texture that needs work and would leave the rest unmarked. - for (auto* texture : storageTextures) { - MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); - m_textureManager->MarkStorageImageTexture(*texture); - } - - Bool anyNeedsPreparation = false; - for (auto* texture : storageTextures) { - if (m_textureManager->NeedsStorageImagePreparation(*texture) || - m_clearManager->HasPendingClear(texture)) { - anyNeedsPreparation = true; - break; - } - } - if (!anyNeedsPreparation) { - return true; - } - - // A first-time storage-usage upgrade recreates the image and carries the old contents - // forward with an out-of-band, immediately-submitted copy (PreserveTextureContentsOnRecreate). - // Whatever this frame already recorded into the old image is still sitting unsubmitted in - // this command buffer, so that copy would read pre-frame content and this frame's rendering - // into the texture would be lost - precisely the render-target-then-image-unit case this - // whole path exists for. Submit what is recorded first; the copy then queues behind it. - Bool anyNeedsStorageUpgrade = false; - for (auto* texture : storageTextures) { - if (m_textureManager->NeedsStorageUsageUpgrade(*texture)) { - anyNeedsStorageUpgrade = true; - break; - } - } - if (anyNeedsStorageUpgrade && HasPendingRecordedWork()) { - if (FlushPendingCommands()) { - // Fresh command buffer: the sampled-descriptor-set memo describes bindings that - // only existed in the retired one. FlushPendingCommands drops the pipeline memo - // itself; this is the other command-buffer-scoped cache. - m_lastSampledSetValid = false; - } else { - // Best effort: the upgrade still produces a correct image, only its preserved - // contents may predate this frame's writes. Dropping the draw would be worse. - MGLOG_E_ONCE("%s: flush before a storage-usage image upgrade failed; preserved contents " - "may be stale for one frame", __func__); - } - } - if (!frame.isCommandRecording) { - m_frameContext.BeginCommandRecording(); - } - - // Image uploads, deferred-clear materialization, and layout barriers are illegal inside - // a classic render pass. Do this before sampler preparation as well: a texture used by - // both a sampler and an image must stay in GENERAL, and both descriptors must name that - // same layout independent of SPIR-V reflection/binding order. - if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { - VkRenderPassManager::EndRenderPass(frame.commandBuffer); - } - - for (auto* texture : storageTextures) { - if (!MaterializePendingClearForTexture(frame.commandBuffer, *texture)) { - MGLOG_E_ONCE("%s: failed to materialize pending clear for storage textureId=%d", - __func__, texture->GetExternalIndex()); - return false; - } - if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) { - MGLOG_E_ONCE("%s: failed to prepare storage textureId=%d", - __func__, texture->GetExternalIndex()); - return false; - } - } - return true; - } - Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots( - FrameContext::FrameData& frame, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - VkPipelineStageFlags consumerShaderStageMask) { - auto& feedbackBindings = m_samplerImageFeedbackScratch; - auto& overrides = m_samplerImageBindingOverridesScratch; - overrides.clear(); - if (!programObj.hasStorageImages) { - feedbackBindings.clear(); - return true; - } - if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) { - MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__, - program.GetExternalIndex()); - return false; - } - - if (feedbackBindings.empty()) { - return true; - } - // Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only - // gets here after an actual sampled/writable-image mip overlap was found, so ordinary - // graphics draws retain the active pass. - if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { - VkRenderPassManager::EndRenderPass(frame.commandBuffer); - } - - struct SnapshotCacheEntry { - MG_State::GLState::ITextureObject* texture = nullptr; - SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown; - VkTextureManager::SampledTextureSnapshot snapshot{}; - }; - Vector snapshotCache; - snapshotCache.reserve(feedbackBindings.size()); - overrides.reserve(feedbackBindings.size()); - for (const auto& feedback : feedbackBindings) { - VkTextureManager::SampledTextureSnapshot snapshot{}; - const auto existing = std::find_if( - snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) { - return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain; - }); - if (existing != snapshotCache.end()) { - snapshot = existing->snapshot; - } else { - if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture, - feedback.numericDomain, consumerShaderStageMask, - snapshot) || - snapshot.imageView == VK_NULL_HANDLE) { - MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__, - feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0, - feedback.samplerBinding, feedback.samplerElement); - return false; - } - snapshotCache.push_back({.texture = feedback.texture, - .numericDomain = feedback.numericDomain, - .snapshot = snapshot}); - } - overrides.push_back({ - .binding = feedback.samplerBinding, - .element = feedback.samplerElement, - .texture = feedback.texture, - .sampler = feedback.sampler, - .imageView = snapshot.imageView, - .imageLayout = snapshot.layout, - .forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger || - feedback.numericDomain == SamplerNumericDomain::UnsignedInteger, - }); - if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) { - MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p", - feedback.samplerBinding, feedback.samplerElement, snapshot.imageView); - } - } - return true; - } - // The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no // per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose // GL scissor test is disabled gets the whole framebuffer, which is exactly "the test always @@ -5897,408 +4153,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return m_baseTransformFlagsCache; } - Bool VulkanRenderer::TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, - Flags aspects, const DrawCmdParam& drawParams, - const IndexBufferView* pIndexBufferView) { - if (!frame.isCommandRecording) { - return false; - } - // Entry select: by the draw program's lifetime id, MRU first (the id pins - // the entry; every other fact is re-guarded below, so probing a stale - // entry can only decline, never serve stale state). - const auto& program = *MG_Pipe::gPipeInputs.GetProgramForDraw(); - const Uint64 programLifetimeId = program.GetLifetimeId(); - SetupDrawSnapshot* snapPtr = nullptr; - { - SetupDrawSnapshot& mru = m_setupDrawSnapshots[m_setupDrawSnapshotMru]; - if (mru.valid && mru.programLifetimeId == programLifetimeId) { - snapPtr = &mru; - } else { - for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) { - SetupDrawSnapshot& candidate = m_setupDrawSnapshots[i]; - if (candidate.valid && candidate.programLifetimeId == programLifetimeId) { - snapPtr = &candidate; - m_setupDrawSnapshotMru = i; - break; - } - } - } - } - if (snapPtr == nullptr) { - return false; - } - SetupDrawSnapshot& snap = *snapPtr; - // Resolved once for the whole function: it guards the snapshot, keys the pipeline memo - // probe below, and is handed to GetOrCreatePipeline on a miss - all three must agree. - const Bool drawPrimitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView); - if (snap.aspects != aspects.GetRaw() || snap.mode != mode || - snap.primitiveRestartEnable != drawPrimitiveRestartEnable) { - return false; - } - if (m_clearManager->HasAnyPendingClears()) { - return false; - } - const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); - if (activeRenderPass == nullptr || activeRenderPass->hash != snap.renderPassHash || - snap.imageIndex != m_imageIndexAcquired) { - return false; - } - if (program.GetBackendStateVersion() != snap.programVersion) { - return false; - } - // glBegin/EndTransformFeedback moves no key this fast path otherwise observes - // (the design makes capture a compile-option FLAG precisely because no version - // bumps, VulkanRenderer.h's pipeline-memo note) - but the snapshot bakes that - // flag into resolvedTransformFlags and the pipeline. Recompute the one dynamic - // bit (the full path's exact predicate) and decline on a mismatch, or the first - // captured draw after glBeginTransformFeedback would bind the undecorated - // variant and silently capture nothing while the CPU bookkeeping advances. - const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled && - MG_Pipe::gPipeInputs.IsTransformFeedbackActive() && - program.GetTransformFeedbackVaryingCount() > 0; - const Bool snapHasXfbCapture = - static_cast(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) & - ProgramFactory::CompileOptionBit::XfbCapture); - if (wantsXfbCapture != snapHasXfbCapture) { - return false; - } - // A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex - // input state (re-resolved below through the layout-keyed memo, so N VAOs - // sharing one attribute layout share one pipeline) and the vertex/index - // buffer binds (re-run every draw anyway). Declining here would send every - // draw of a VAO-cycling stream (Minecraft chunk rendering) through the full - // path, re-resolving descriptors and texture layouts nothing invalidated. - const auto& vao = *MG_Pipe::gPipeInputs.GetBoundVertexArray(); - // P2 D12.4: the handle replaces the (address, lifetime id) pair here too - one - // compare instead of two, and the same identity the VAO draw memo is keyed on, so - // the two cannot disagree about whether "the VAO moved". The config version stays: - // it answers a different question (did this same object's layout change). - const Bool vaoMoved = - MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput) - ? (!(ResolveVaoHandle(vao) == snap.vaoHandle) || - vao.GetConfigVersion() != snap.vaoConfigVersion) - : (static_cast(&vao) != snap.vao || - vao.GetLifetimeId() != snap.vaoLifetimeId || - vao.GetConfigVersion() != snap.vaoConfigVersion); - const auto& drawFbo = - MG_Pipe::gPipeInputs.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); - if (static_cast(drawFbo.get()) != snap.drawFbo || - drawFbo->GetLifetimeId() != snap.drawFboLifetimeId || - drawFbo->GetObjectVersion() != snap.fboVersion) { - return false; - } - // The two monotonic counters get a shadow-compare rescue instead of an - // unconditional decline: both bump on state writes whose VALUE often lands - // back on what the snapshot already describes (a GL_BLEND toggle between - // two draws, a redundant glBindSampler), and declining here sends every - // such draw through the full SetupDraw. - const Uint renderStateVersion = MG_Pipe::gPipeInputs.GetPipelineStateVersion(); - const Uint64 bindGeneration = MG_Pipe::gPipeInputs.GetTextureBindGeneration(); - const Bool renderStateMoved = renderStateVersion != snap.renderStateVersion; - const Bool bindsMoved = bindGeneration != snap.bindGeneration; - if (renderStateMoved) { - // Only the pipeline depends on the moved state - except the render-pass - // flavor input (depth/stencil participation); a flip of that must take - // the full path's pass selection. One bulk parameters fetch instead of - // two capability-accessor calls; both are pure reads of the same fields. - const RenderStateParameters& rsp = MG_Pipe::gPipeInputs.GetRenderStateParameters(); - const Bool drawUsesDepthStencil = rsp.DepthTestEnabled || rsp.StencilTestEnabled; - if (drawUsesDepthStencil != snap.drawUsesDepthStencil) { - return false; - } - } - // The FBO identity+version compare above proved this draw's framebuffer is the - // snapshotting draw's, so its default-ness is the snapshot's too - no second walk - // of the binding slot and no virtual IsDefaultFramebuffer call. - if (GetBaseTransformFlagsRaw(snap.drawFboIsDefault) != snap.baseTransformFlags) { - return false; - } - if (m_textureManager->GetResourceEraseEpoch() != snap.textureEraseEpoch || - m_textureManager->GetTextureImageEpoch() != snap.textureImageEpoch || - m_renderPassManager->GetRenderbufferImageEpoch() != snap.renderbufferImageEpoch) { - return false; - } - // Program entry: (lifetimeId, backend-state version, resolved flags) were proven - // equal above, and those pin the factory hash - so the snapshot's memoised entry - // pointer IS this draw's entry while the factory's open-addressing cache has not - // moved entries (structure epoch). Bypassing GetOrCreateProgram skips its use - // stamp, so re-stamp here or the idle sweep could evict a live entry. - const ProgramFactory::VkProgramObject* programObjPtr = snap.programObj; - if (programObjPtr != nullptr && - snap.programFactoryEpoch == m_programFactory->GetCacheStructureEpoch()) { - m_programFactory->StampProgramUse(*programObjPtr); - } else { - programObjPtr = &m_programFactory->GetOrCreateProgram( - program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags)); - snap.programObj = programObjPtr; - snap.programFactoryEpoch = m_programFactory->GetCacheStructureEpoch(); - } - const auto& programObj = *programObjPtr; - // Pinned for BeginXfbCaptureForDraw, which otherwise decides from GL state alone and has - // no way to know the bound pipeline's last pre-rasterization module lost (or never got) - // its Xfb execution mode. See VkProgramObject::xfbCaptureDeclined. - m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined; - // A refused program cannot reach here today - the full path refuses before it ever - // records a snapshot - but declining the fast path costs one compare and means the - // refusal does not depend on that ordering staying true. - if (programObj.pointSizeCapabilityUnsupported || programObj.invalidSpirvDeclined) { - return false; - } - - // The pipeline and the vertex-input pre-flight depend on the VAO only through - // its resolved LAYOUT (layoutHash folds the attribute formats, bindings and the - // unsupported mask; the masks below are functions of the same configuration), - // never its identity. A VAO-cycling stream (Minecraft chunk rendering) swaps - // hundreds of VAOs sharing one layout per frame: answer "same layout?" from the - // VAO's aux memo - it sits next to the config-version word this compare chain - // already loaded - instead of chasing the vertex-input factory's cold heap entry. - Uint64 vaoLayoutHash = snap.vaoLayoutHash; - Bool vaoLayoutMoved = false; - if (vaoMoved) { - // Read the layout facts through the flat per-VAO memo table, keyed by the - // VAO's content-hash memo. The hash memo shares the cache line this compare - // chain already loaded (the config version), and the table slot is compact - // and hot - unlike the VAO's aux-memo words, which start a second cold line - // of every object in a VAO-cycling frame. The slot only ever answers for - // THIS object: LookupVaoDrawMemo matches (address, lifetime id), so a slot - // a destroyed VAO left behind at a recycled address misses and the facts - // are re-resolved. The contentHash compare is the second gate on top of - // that identity check, catching a reconfiguration of the same live object. - Uint64 auxMasks = 0; - Bool factsKnown = false; - Uint64 contentHash = 0; - if (VaoContentHashIfKnown(vao, contentHash)) { - const VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao); - if (vaoMemo->layoutFactsValid && vaoMemo->contentHash == contentHash) { - vaoLayoutHash = vaoMemo->layoutHash; - auxMasks = vaoMemo->layoutAuxMasks; - factsKnown = true; - } - } - if (!factsKnown) { - // First sight of this VAO configuration: resolve (which stamps the - // VAO's hash memo) and read the same facts from the entry, then stamp - // the table slot for every later draw. - const auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); - vaoLayoutHash = vertexInputState.layoutHash; - auxMasks = VertexInputStateFactory::PackVertexInputAuxMasks( - vertexInputState.unsupportedAttribMask, vertexInputState.attributeLocationMask); - Uint64 stampedHash = 0; - if (VaoContentHashIfKnown(vao, stampedHash)) { - VaoDrawMemo* vaoMemo = LookupVaoDrawMemo(&vao); - vaoMemo->contentHash = stampedHash; - vaoMemo->layoutHash = vaoLayoutHash; - vaoMemo->layoutAuxMasks = auxMasks; - vaoMemo->layoutFactsValid = true; - } - } - vaoLayoutMoved = vaoLayoutHash != snap.vaoLayoutHash; - if (vaoLayoutMoved) { - // Vertex-input pre-flight for the changed layout, mirroring the full - // path: a bad attribute must never be baked into a cached VkPipeline, - // and the current-value synthesis in UploadAndBindVertexBuffers must - // never see an unsupported generic-attribute type. Declining routes the - // draw through the full path's loud failure reporting. An UNMOVED layout - // needs no pre-flight: the snapshotting draw passed it with identical - // inputs (same program; masks pinned by the layout hash). - const Uint32 unsupportedAttribMask = static_cast(auxMasks >> 32); - const Uint32 attributeLocationMask = static_cast(auxMasks); - const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; - if ((unsupportedAttribMask & activeAttribMask) != 0) { - return false; - } - const Uint32 missingAttribMask = activeAttribMask & ~attributeLocationMask; - if (missingAttribMask != 0) { - for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { - if ((missingAttribMask & (1u << location)) == 0) { - continue; - } - if (MG_State::GLState::ClassifyVertexAttribType(programObj.vertexInputTypes[location]) - .baseType == MG_State::GLState::VertexAttribBaseType::Unsupported) { - return false; - } - } - } - } - } - if (bindsMoved && - !m_uniformManager->SampledBindingsUnchanged(program, programObj, snap.sampledBindingRecords)) { - return false; - } - - // Same sampled set as the snapshotting draw (program/bind keys above); - // verify content and params are untouched and every layout is still - // sampleable, then stamp recording use exactly as the full path would. - // A feedback case (sampled texture written by the active pass) fails the - // layout check and falls back to the full path's end-pass handling. - // The ENTRY's copies, not the scratch vectors: with more than one entry - // the scratch holds only the last full-path draw's set, which may belong - // to a different program. - const auto& sampledTextures = snap.sampledTextures; - const auto& sampledResources = snap.sampledResources; - if (sampledResources.size() != sampledTextures.size()) { - return false; - } - Uint64 contentSum = 0; - Uint64 paramsSum = 0; - // The descriptor-reuse hint (see BindProgramUniformBuffers) additionally needs - // every sampled resource still in the exact layout the cached descriptors hold. - // A layout that moved to a different-but-sampleable one only clears the hint - // (this draw re-resolves and re-caches) - the fast path itself stays valid. - // bindsMoved does not clear the hint: reaching this point with a moved bind - // generation means SampledBindingsUnchanged proved the per-binding (texture, - // sampler) pairs identical, and the sums/generation checks below cover every - // remaining descriptor input. - const Bool layoutSnapshotUsable = snap.sampledLayouts.size() == sampledTextures.size(); - Bool samplerDescriptorsUnchanged = layoutSnapshotUsable; - for (SizeT i = 0; i < sampledTextures.size(); ++i) { - const auto* sampledTexture = sampledTextures[i]; - if (sampledTexture == nullptr) { - continue; - } - auto* resource = sampledResources[i]; - if (resource == nullptr || !IsValidSampledImageLayout(resource->layout)) { - return false; - } - if (layoutSnapshotUsable && snap.sampledLayouts[i] != resource->layout) { - snap.sampledLayouts[i] = resource->layout; - samplerDescriptorsUnchanged = false; - } - contentSum += sampledTexture->GetContentVersion(); - paramsSum += sampledTexture->GetTextureParamsVersion(); - // Folded into this walk (was a second loop): the stamp is a plain recency - // store. Stamping ahead of the sum compare below is benign - a declined - // draw re-runs the full path, which stamps the same resources, and an - // over-stamp only delays garbage collection by one generation. - m_textureManager->StampResourceRecordingUse(*resource); - } - if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) { - return false; - } - const Uint64 samplingResolutionGeneration = MG_Pipe::gPipeInputs.GetSamplingResolutionGeneration(); - if (samplingResolutionGeneration != snap.samplingResolutionGeneration) { - // Decline, not re-arm: snap.resolvedTransformFlags bakes the - // ExplicitLod0Sampling verdict, which reads the effective sampler's - // filters/aniso/LOD range - exactly the state this counter tracks. - // Re-arming the stamp here would rebuild the descriptors but keep the - // stale SPIR-V variant forever (every later draw compares equal again). - // Same shape as the erase-epoch declines above; costs one full-path draw - // per sampler/shape change, and the full path's LOD memo re-probes. - return false; - } - - // Everything the full path would re-resolve is provably unchanged - or, for - // a moved pipeline-state version or a changed vertex-input LAYOUT, reduces to - // re-resolving just the pipeline through the value-keyed memo against the - // still-active render pass. A changed VAO with the SAME layout keeps the - // snapshot's pipeline outright (the layout is the pipeline's only VAO input). - // Run only the per-draw tail. - VkPipeline pipeline = snap.pipeline; - if (renderStateMoved || vaoLayoutMoved) { - pipeline = VK_NULL_HANDLE; - // The render pass is provably the snapshot's (hash match above), so probe - // the value-keyed pipeline memo directly - no render-pass-entry re-fetch - // (whose pending-clear probes cost more than the whole probe below). For a - // moved state version, first refresh the pipeline-state VALUE hash exactly - // as GetOrCreatePipeline would (same inputs: the snapshot pins the pass, so - // its color attachment count is the right hash input); the value hash is - // what lets a per-draw GL_BLEND toggle alternate between two memo entries - // instead of missing forever on a monotonic version. A miss falls through - // to the full lookup. - // Same arm selector as GetOrCreatePipeline's probe (P2 D12.1); this site is the - // fast path's copy of it, and the two must key identically or the fast path would - // hand back a pipeline the full path would not have matched. - const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso(); - const Uint64 pipelineStateHash = - !MG_Pipe::MGPipeHandleIsNull(renderStateCso) - ? 0 - : ResolveFallbackPipelineStateHash(renderStateVersion, snap.renderPassColorCount, - snap.renderPassSampleCount); - const auto memoTransformFlags = - ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags); - for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) { - const PipelineMemoEntry& entry = m_pipelineMemo[i]; - if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && - entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash && - entry.renderPassHash == snap.renderPassHash && - entry.wireRenderPassCompatibilityId == 0 && - entry.pipelineStateHash == pipelineStateHash && - entry.renderStateCso == renderStateCso && - entry.primitiveRestartEnable == drawPrimitiveRestartEnable && - entry.transformFlags == memoTransformFlags) { - pipeline = entry.pipeline; - break; - } - } - if (pipeline == VK_NULL_HANDLE) { - // Same lookup the full path would do; every input (FBO + version, image - // index, depth/stencil participation, image epochs, no pending clears) - // was verified unchanged above, so this is a pure cache hit on the same - // entry the snapshot's pipeline was built against. - const RenderPassEntry* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass( - *drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil); - // A decline (nullptr) is an attachment DirectVulkan cannot represent; the builder - // has already logged it. Fall out of the fast path the same way an incompatible - // pass does - the full path re-resolves, declines again and drops the draw. - if (renderPassEntry == nullptr || !activeRenderPass->CompatibleWith(*renderPassEntry)) { - return false; - } - pipeline = GetOrCreatePipeline(mode, program, programObj, - ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags), - vao, *renderPassEntry, drawPrimitiveRestartEnable); - if (pipeline == VK_NULL_HANDLE) { - return false; - } - } - } - // Every decline is behind us: the snapshot again describes the current - // counters, so the next draw's compare is two integer loads. - snap.renderStateVersion = renderStateVersion; - snap.bindGeneration = bindGeneration; - snap.vao = static_cast(&vao); - snap.vaoLifetimeId = vao.GetLifetimeId(); - // Guarded by the SUBSYSTEM, not only by the build switch: with bit 6 clear the field - // is dead (vaoMoved takes the address/lifetime-id branch), and minting a handle for it - // would put this package's cost inside MOBILEGL_PIPE_PUSH=0 - the all-pull control arm - // D14 defines as reproducing P1 exactly, and the arm D.4.3's T2 is measured on. - if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) { - snap.vaoHandle = ResolveVaoHandle(vao); - } - snap.vaoConfigVersion = vao.GetConfigVersion(); - snap.vaoLayoutHash = vaoLayoutHash; - snap.pipeline = pipeline; - if (!g_dynamicStateShadow->graphicsPipelineValid || - g_dynamicStateShadow->graphicsPipeline != pipeline) { - vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - g_dynamicStateShadow->graphicsPipelineValid = true; - g_dynamicStateShadow->graphicsPipeline = pipeline; - } - if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, - m_frameContext.GetCurrentFrameIndex(), - VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, - samplerDescriptorsUnchanged)) { - return false; - } - if (!UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView)) { - return false; - } - if (aspects & DrawSetupAspect::IndexBuffer) { - const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView); - MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer"); - } - ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount); - if (MG_Util::PipeStats::Enabled()) { - // The six accessor reads this function makes unconditionally on the path that - // reaches here: the draw program, the VAO, the draw-FBO slot, the pipeline - // state version, the texture bind generation and the sampling-resolution - // generation. The XFB-active probe is elided on a device without the feature - // and the parameter-block fetch only runs when the pipeline state version - // moved, so neither is counted (lower bound, as everywhere else). - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 6); - } - return true; - } - Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { @@ -7184,7 +5038,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } m_frameContext.BeginCommandRecording(); // New command buffer: the same reset SetupDraw applies when it begins one. - m_lastSampledSetValid = false; return true; } @@ -9220,127 +7073,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } - void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, - GLsizei width, GLsizei height, GLenum format, GLenum type, - void* pixels) { - if (width <= 0 || height <= 0) { - return; - } - - const Bool wantDepth = format != GL_STENCIL_INDEX; - const Bool wantStencil = format != GL_DEPTH_COMPONENT; - // GL_DEPTH_STENCIL requires both halves; the state layer already rejected - // framebuffers lacking either, so resolving via the depth attachment is enough. - const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth - : MobileGL::FramebufferAttachmentType::Stencil; - const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer(); - if (!readIsDefaultFbo) { - const auto& attachment = readFbo.GetAttachment(attachmentType); - if (!attachment.IsValid() || attachment.IsEmpty()) { - MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image"); - return; - } - } - - auto& frame = m_frameContext.GetCurrent(); - if (!frame.isCommandRecording) { - m_frameContext.BeginCommandRecording(); - } - if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { - VkRenderPassManager::EndRenderPass(frame.commandBuffer); - } - - // The default framebuffer's depth/stencil lives in the swapchain, not in an - // attachment object: its placeholder ITextureObject describes the format but backs no - // image, so the branches below would have synced (and read back) an unrelated one. - // Declining outright is what made every glReadPixels(GL_DEPTH_COMPONENT/ - // GL_STENCIL_INDEX) of the default framebuffer leave the caller's buffer untouched - - // the whole KHR-GL*.framebuffer_blit family checks exactly that before it blits. - if (readIsDefaultFbo) { - // A read names the image the app last rendered into, not the one Present() has - // already acquired (see m_defaultFramebufferImageIndex). - const Uint32 srcImageIndex = DefaultFramebufferReadIndex(true); - const VkImage swapchainDepthImage = m_swapchainObject.GetDepthStencilImage(srcImageIndex); - if (swapchainDepthImage == VK_NULL_HANDLE) { - MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: the default framebuffer has no " - "depth/stencil image"); - return; - } - // Per aspect, because the default framebuffer carries a SEPARATE placeholder - // attachment for depth and for stencil (MG_Impl/Init.cpp) and each parks its own - // pending clear; materializing only one would read the other back un-cleared. - if (wantDepth) { - const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( - frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Depth); - MOBILEGL_ASSERT(clearReady, - "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " - "depth clear"); - } - if (wantStencil) { - const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( - frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Stencil); - MOBILEGL_ASSERT(clearReady, - "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " - "stencil clear"); - } - const VkFormat swapchainDepthFormat = m_swapchainObject.GetDepthStencilFormat(); - VkImageLayout trackedLayout = m_swapchainObject.GetDepthStencilImageLayout(srcImageIndex); - ReadDepthStencilImageToClient(swapchainDepthImage, swapchainDepthFormat, &trackedLayout, - GetDepthStencilAspectMaskForFormat(swapchainDepthFormat), 0, 0, x, y, - width, height, format, type, pixels, - /*defaultFramebufferOrientation=*/true); - m_swapchainObject.SetDepthStencilImageLayout(srcImageIndex, trackedLayout); - return; - } - - const auto& attachment = readFbo.GetAttachment(attachmentType); - VkImage image = VK_NULL_HANDLE; - VkFormat vkFormat = VK_FORMAT_UNDEFINED; - VkImageLayout* trackedLayout = nullptr; - VkImageAspectFlags imageAspect = VK_IMAGE_ASPECT_NONE; - Uint32 mipLevel = 0; - Uint32 baseArrayLayer = 0; - if (attachment.IsTexture() && attachment.GetTexture()) { - auto textureObject = attachment.GetTexture(); - const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *textureObject); - MOBILEGL_ASSERT(clearReady, "ReadDepthStencilPixels: failed to materialize pending clear for textureId=%d", - textureObject->GetExternalIndex()); - auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject); - if (resource == nullptr || resource->image == VK_NULL_HANDLE) { - MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to sync depth textureId=%u", - textureObject->GetExternalIndex()); - return; - } - image = resource->image; - vkFormat = resource->format; - trackedLayout = &resource->layout; - imageAspect = resource->aspect; - mipLevel = ToStorageMipLevel(attachment.GetTexture().get(), attachment.GetTextureLevel()); - baseArrayLayer = ToStorageArrayLayer(attachment.GetTexture().get(), attachment.GetTextureLayer()); - } else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) { - const auto& renderbufferObject = attachment.GetRenderbuffer(); - const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, renderbufferObject); - MOBILEGL_ASSERT(clearReady, - "ReadDepthStencilPixels: failed to materialize pending clear for renderbuffer %u", - renderbufferObject->GetExternalIndex()); - auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbufferObject); - if (resource == nullptr || resource->image == VK_NULL_HANDLE) { - MGLOG_E_ONCE("DirectVulkan::ReadDepthStencilPixels skipped: failed to resolve renderbuffer %u", - renderbufferObject->GetExternalIndex()); - return; - } - image = resource->image; - vkFormat = resource->format; - trackedLayout = &resource->layout; - imageAspect = resource->aspect; - } else { - return; - } - - ReadDepthStencilImageToClient(image, vkFormat, trackedLayout, imageAspect, mipLevel, baseArrayLayer, x, y, - width, height, format, type, pixels); - } - void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, @@ -10630,267 +8362,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { EmitMultiDrawIndexed(frame.commandBuffer, pParams, drawCount); } - // Byte size of the command structures GL defines for the indirect draws (GL 4.6 core - // 10.3.10): four uint32 for DrawArraysIndirectCommand, five for DrawElementsIndirect- - // Command. These bound the read out of GL_DRAW_INDIRECT_BUFFER and are the default - // stride, so they must be GL's sizes and not this renderer's own draw-parameter - // structs - DrawCmdParam carries two extra members and is 24 bytes, which made every - // glDrawArraysIndirect on a tightly-sized indirect buffer look out of range and draw - // nothing. - constexpr SizeT kGLDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32); - constexpr SizeT kGLDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32); - - void VulkanRenderer::MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, - GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { - auto& frame = m_frameContext.GetCurrent(); - - if (maxdrawcount <= 0) { - return; - } - if (stride == 0) { - stride = kGLDrawElementsIndirectCommandBytes; - } - if (stride < static_cast(kGLDrawElementsIndirectCommandBytes)) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: stride %d is smaller than command size %zu", - stride, kGLDrawElementsIndirectCommandBytes); - return; - } - - const SizeT indexSize = MG_Util::GetGLTypeSize(type); - if (indexSize == 0) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: unsupported index type 0x%x", type); - return; - } - - const auto& vao = *MG_Pipe::gPipeInputs.GetBoundVertexArray(); - const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); - if (!indexBuffer) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: no element array buffer is bound"); - return; - } - - const SizeT commandOffset = reinterpret_cast(indirect); - const SizeT commandBytes = commandOffset + - static_cast(stride) * static_cast(maxdrawcount - 1) + kGLDrawElementsIndirectCommandBytes; - auto drawBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); - return; - } - - auto parameterBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); - if (!parameterBuffer || static_cast(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); - return; - } - - DrawCmdParam vertexRange{}; - vertexRange.vertexCount = static_cast(indexBuffer->GetSize() / indexSize); - vertexRange.instanceCount = 1; - - IndexBufferView indexBufferView{}; - indexBufferView.indexType = type; - indexBufferView.indexByteOffset = 0; - indexBufferView.indexByteSize = indexBuffer->GetSize(); - - if (!SetupDraw(frame, mode, DrawSetupAspect::IndexBuffer | DrawSetupAspect::IndirectDrawBuffer, - vertexRange, &indexBufferView)) { - return; - } - - drawBuffer->SyncPersistentMappedRange(); - parameterBuffer->SyncPersistentMappedRange(); - - BufferSlice drawSlice{}; - if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: failed to sync draw indirect buffer"); - return; - } - BufferSlice parameterSlice{}; - if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, parameterBuffer, parameterSlice)) { - MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: failed to sync parameter buffer"); - return; - } - - MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); - // vkCmdDrawIndexedIndirectCount with maxDrawCount > 1 additionally requires the - // multiDrawIndirect device feature; fall back to the CPU readback loop otherwise. - if (m_drawIndirectCountExtensionEnabled && s_vkCmdDrawIndexedIndirectCount && - (m_multiDrawIndirectFeatureEnabled || maxdrawcount == 1)) { - MGLOG_D("DirectVulkan: glMultiDrawElementsIndirectCountARB(max=%d stride=%d)", maxdrawcount, stride); - s_vkCmdDrawIndexedIndirectCount(frame.commandBuffer, - drawSlice.buffer, - drawSlice.offset + static_cast(commandOffset), - parameterSlice.buffer, - parameterSlice.offset + static_cast(drawcount), - static_cast(maxdrawcount), - static_cast(stride)); - return; - } - - const Uint8* parameterData = parameterBuffer->MappedData(); - const Uint8* drawData = drawBuffer->MappedData(); - Uint32 actualDrawCount = 0; - std::memcpy(&actualDrawCount, parameterData + drawcount, sizeof(actualDrawCount)); - actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); - for (Uint32 idraw = 0; idraw < actualDrawCount; ++idraw) { - DrawIndexedCmdParam cmd{}; - std::memcpy(&cmd, drawData + commandOffset + static_cast(idraw) * stride, sizeof(cmd)); - vkCmdDrawIndexed(frame.commandBuffer, cmd.indexCount, cmd.instanceCount, cmd.firstIndex, - cmd.vertexOffset, cmd.firstInstance); - } - } - - void VulkanRenderer::MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, - GLsizei drawcount, GLsizei stride) { - auto& frame = m_frameContext.GetCurrent(); - - if (drawcount <= 0) { - return; - } - if (stride == 0) { - stride = kGLDrawElementsIndirectCommandBytes; - } - if (stride < static_cast(kGLDrawElementsIndirectCommandBytes)) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: stride %d is smaller than command size %zu", - stride, kGLDrawElementsIndirectCommandBytes); - return; - } - - const SizeT indexSize = MG_Util::GetGLTypeSize(type); - if (indexSize == 0) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: unsupported index type 0x%x", type); - return; - } - - const auto& vao = *MG_Pipe::gPipeInputs.GetBoundVertexArray(); - const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); - if (!indexBuffer) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: no element array buffer is bound"); - return; - } - - const SizeT commandOffset = reinterpret_cast(indirect); - const SizeT commandBytes = commandOffset + - static_cast(stride) * static_cast(drawcount - 1) + kGLDrawElementsIndirectCommandBytes; - auto drawBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); - return; - } - - // The command parameters live on the GPU; the CPU-visible range that any single - // command may address is the whole element array buffer. - DrawCmdParam vertexRange{}; - vertexRange.vertexCount = static_cast(indexBuffer->GetSize() / indexSize); - vertexRange.instanceCount = 1; - - IndexBufferView indexBufferView{}; - indexBufferView.indexType = type; - indexBufferView.indexByteOffset = 0; - indexBufferView.indexByteSize = indexBuffer->GetSize(); - - if (!SetupDraw(frame, mode, DrawSetupAspect::IndexBuffer | DrawSetupAspect::IndirectDrawBuffer, - vertexRange, &indexBufferView)) { - return; - } - - BufferSlice drawSlice{}; - if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { - MGLOG_E_ONCE("MultiDrawElementsIndirect skipped: failed to sync draw indirect buffer"); - return; - } - - MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); - MGLOG_D("DirectVulkan: glMultiDrawElementsIndirect(drawcount=%d stride=%d)", drawcount, stride); - if (drawcount == 1 || - (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) { - vkCmdDrawIndexedIndirect(frame.commandBuffer, - drawSlice.buffer, - drawSlice.offset + static_cast(commandOffset), - static_cast(drawcount), - static_cast(stride)); - return; - } - - // multiDrawIndirect device feature unavailable: one indirect draw per command is - // valid without it and still consumes the GPU-written parameters. - for (GLsizei idraw = 0; idraw < drawcount; ++idraw) { - vkCmdDrawIndexedIndirect(frame.commandBuffer, - drawSlice.buffer, - drawSlice.offset + static_cast(commandOffset) + - static_cast(idraw) * static_cast(stride), - 1, 0); - } - } - - void VulkanRenderer::MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, - GLsizei stride) { - auto& frame = m_frameContext.GetCurrent(); - - if (drawcount <= 0) { - return; - } - if (stride == 0) { - stride = kGLDrawArraysIndirectCommandBytes; - } - if (stride < static_cast(kGLDrawArraysIndirectCommandBytes)) { - MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", - stride, kGLDrawArraysIndirectCommandBytes); - return; - } - - const SizeT commandOffset = reinterpret_cast(indirect); - const SizeT commandBytes = commandOffset + - static_cast(stride) * static_cast(drawcount - 1) + kGLDrawArraysIndirectCommandBytes; - auto drawBuffer = MG_Pipe::gPipeInputs.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - if (!drawBuffer || commandBytes > drawBuffer->GetSize()) { - MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); - return; - } - - // The command parameters live on the GPU, so the vertex range is unknown here; - // resident vertex buffers are uploaded in full regardless. A CLIENT-MEMORY ARRAY is the - // one exception: its bytes have to be staged BEFORE the GPU reads the command, so for - // exactly that case the words are resolved on the CPU and this range is what bounds the - // staging. The draw itself stays native - the GPU reads the same commands. - DrawCmdParam vertexRange{}; - vertexRange.vertexCount = 0; - vertexRange.instanceCount = 1; - // MONOLITH ONLY: with a transport the frontend VAO is not this side's to read at all - // (the client owns those bytes and stages them itself), and this runs on the apply thread. - - if (!SetupDraw(frame, mode, DrawSetupAspect::IndirectDrawBuffer, vertexRange)) { - return; - } - - BufferSlice drawSlice{}; - if (!m_bufferManager.AcquireResidentSlice(BufferKind::Indirect, drawBuffer, drawSlice)) { - MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: failed to sync draw indirect buffer"); - return; - } - - MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); - MGLOG_D("DirectVulkan: glMultiDrawArraysIndirect(drawcount=%d stride=%d)", drawcount, stride); - if (drawcount == 1 || - (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled && stride % 4 == 0)) { - vkCmdDrawIndirect(frame.commandBuffer, - drawSlice.buffer, - drawSlice.offset + static_cast(commandOffset), - static_cast(drawcount), - static_cast(stride)); - return; - } - - for (GLsizei idraw = 0; idraw < drawcount; ++idraw) { - vkCmdDrawIndirect(frame.commandBuffer, - drawSlice.buffer, - drawSlice.offset + static_cast(commandOffset) + - static_cast(idraw) * static_cast(stride), - 1, 0); - } - } - VkCommandBuffer VulkanRenderer::AcquireBufferCopyCommandBuffer() { if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) { return VK_NULL_HANDLE; @@ -11231,9 +8702,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_textureManager->BeginFrame(frameIndex); } m_bufferManager.BeginFrame(frameIndex); - // The cached conversion slices point into the transient arena the - // BeginFrame above just rewound; drop them together. - m_convertedVertexStreams.clear(); if (m_renderPassManager) { m_renderPassManager->OnPresent(); } @@ -11245,15 +8713,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_pipelineFactory && m_pipelineFactory->OnFrameBoundary() > 0) { InvalidatePipelineMemo(); } - if (m_vertexInputStateFactory) { - m_vertexInputStateFactory->OnFrameBoundary(); - } - // Reclaim {slot, gen} for objects that have not been drawn for a long time, on the same - // cadence and the same retirement age as the entries those slots key. This is the - // stand-in for the frontend death notification P2 has no hook for, and it is what keeps - // the mint's footprint the LIVE working set rather than every object ever created - // (review v2 MAJOR 1 / MAJOR 3). - m_pipeIdentity.OnFrameBoundary(); if (m_samplerManager) { m_samplerManager->OnFrameBoundary(); } @@ -11506,7 +8965,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return; } // Fresh command buffer: the same reset every other mid-frame flush site applies. - m_lastSampledSetValid = false; ++m_oversizedRecordingSplits; MGLOG_I_ONCE("DirectVulkan: a frame recorded more than %u draws into one command buffer; submitting it and " "continuing on a fresh one (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)", @@ -11563,7 +9021,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_drawsInRecording = 0; // Dynamic state does not survive a command-buffer boundary. ResetDynamicStateShadow(); - InvalidateSetupDrawSnapshots(); if (m_uniformManager) { m_uniformManager->OnCommandBufferBoundary(); } @@ -11750,10 +9207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // A recreated pipeline could reuse a freed handle value and alias // the bind-dedup shadow; force the next draw to re-bind. g_dynamicStateShadow->graphicsPipelineValid = false; - InvalidateSetupDrawSnapshots(); } - m_vertexInputStateFactory->OnFrameBoundary(); - m_pipeIdentity.OnFrameBoundary(); m_samplerManager->OnFrameBoundary(); auto& frame = m_frameContext.GetCurrent(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); @@ -11944,7 +9398,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { ClearWireDrawPassCache(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_convertedVertexStreams.clear(); // Descriptor-set reuse cursors rewind exactly once per frame, here, // after the slot's fence wait proved its previous sets GPU-idle. (The // per-draw-path lazy rewind missed frames whose recording was opened @@ -14098,7 +11551,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } InvalidatePipelineMemo(); // pipelines freed -> the memoized handle would dangle g_dynamicStateShadow->graphicsPipelineValid = false; - InvalidateSetupDrawSnapshots(); DestroyComputePipelines(); if (m_frameContext.GetFrameCount() > 0) { m_frameContext.GetCurrent().isCommandRecording = false; @@ -14115,7 +11567,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); - m_convertedVertexStreams.clear(); } return true; } @@ -14224,7 +11675,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_renderPassManager) m_renderPassManager->SetDefaultFramebufferTarget(m_activeTargetSerial); InvalidatePipelineMemo(); ResetDynamicStateShadow(); - InvalidateSetupDrawSnapshots(); } Bool VulkanRenderer::ActivateSurfaceTarget(Uint64 key, NativeWindowType window, @@ -14330,7 +11780,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (m_renderPassManager) m_renderPassManager->PurgeRenderPasses(); InvalidatePipelineMemo(); ResetDynamicStateShadow(); - InvalidateSetupDrawSnapshots(); DestroyParkedTarget(parked->second); m_parkedTargets.erase(parked); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 8c91765a..857af4de 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -243,14 +243,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView = nullptr); - // ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully - // resolved draw configuration; the next draw whose cheap version/identity - // checks all match skips the resolution half (LOD probe, sampled-set - // walk, render-pass and pipeline resolution) and jumps straight to the - // per-draw tail. Returns false (leaving no side effects that the full - // path cannot redo idempotently) whenever anything might have changed. - Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags aspects, - const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView); void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry& compatibleRenderPassEntry); @@ -295,11 +287,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Server resource handle in, tightly packed owned bytes out. No frontend // texture, pixel-pack state or PBO is consulted by this readback. Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector& ownedBytes); - // GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the - // read framebuffer's depth/stencil attachment (per-aspect buffer copies with - // CPU repacking into the requested client layout). - void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width, - GLsizei height, GLenum format, GLenum type, void* pixels); // Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage; // expects command recording to be active and any render pass already ended. // @@ -363,11 +350,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { void DrawElements(const DrawIndexedCmd& payload); void MultiDrawArrays(const MultiDrawCmd& payload); void MultiDrawElements(const MultiDrawIndexedCmd& payloads); - void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, - GLsizei stride); - void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); - void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, - GLsizei maxdrawcount, GLsizei stride); void Present(); const PhysicalDevice& GetPhysicalDevice() const; @@ -1544,432 +1526,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { DepthMipmapResources m_depthMipmapResources; Vector m_deferredDepthMipmapCleanup; - // Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled - // texture SET is provably unchanged from the previous draw: same program (lifetime id + - // backend-state version, which covers sampler-uniform reassignment / relink) and transform - // flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that - // moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration - // - membership depends on mipmap-completeness, which both of those decide). On a hit, - // m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback / - // layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL - // walk is skipped. The program lifetime id (never reused, unlike the GL name) and the - // monotonic bind generation make the key ABA-proof; the per-command-buffer reset is a cheap - // belt-and-suspenders. - Bool m_lastSampledSetValid = false; - Uint64 m_lastSampledSetProgramLifetimeId = 0; - Uint32 m_lastSampledSetProgramVersion = 0; - ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; - Uint64 m_lastSampledSetBindGeneration = 0; - Uint64 m_lastSampledSetSamplingGeneration = 0; // Set from the draw's resolved VkProgramObject on both the full and the fast setup paths; // read by BeginXfbCaptureForDraw, which has only GL state otherwise. See // VkProgramObject::xfbCaptureDeclined. Bool m_currentDrawXfbCaptureDeclined = false; - // Memo for the per-draw explicit-LOD-0 eligibility probe - // (ProgramSamplesOnlySingleLevelTextures): same key family as the - // sampled-set memo, plus the sampled textures' params-version sum so a - // level-range or filter change re-probes. On a hit the resolved - // transform flags are reused, which also collapses the two - // GetOrCreateProgram lookups into one. - Bool m_lastLodDecisionValid = false; - Uint64 m_lastLodProgramLifetimeId = 0; - Uint32 m_lastLodProgramVersion = 0; - Uint64 m_lastLodBindGeneration = 0; - Uint64 m_lastLodParamsSum = 0; - // Sampling-resolution generation at probe time. The probe reads the effective - // sampler's filters/aniso/LOD range, whose setters bump only this counter - - // the params-version sum above never moves for them. - Uint64 m_lastLodSamplingGeneration = 0; - ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {}; - ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {}; - - // Does the current program's vertex stage declare the BaseVertex builtin? A property - // of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key. - // - // Memoized rather than re-asked because asking means resolving the UN-zeroed program - // variant, and a program that only ever draws non-indexed would then compile a variant - // no draw uses AND re-stamp its use every draw, so the idle sweep could never retire - // it. With the memo the answer is known before the first lookup and only the variant - // the draw actually needs is resolved. - Bool m_lastBaseVertexQueryValid = false; - Uint64 m_lastBaseVertexProgramLifetimeId = 0; - Uint32 m_lastBaseVertexProgramVersion = 0; - Bool m_lastBaseVertexReads = false; - - // Snapshot behind TrySetupDrawFastPath. Values only: the program and - // render-pass caches are open-addressing maps whose entries move on - // insert, so no pointers into them are cached; the pipeline handle is - // protected by the command-buffer-boundary reset plus the mid-frame - // pipeline-destruction resets, and monotonic epochs guard everything - // that can be destroyed or recreated between draws. - struct SetupDrawSnapshot { - Bool valid = false; - Uint8 aspects = 0; - GLenum mode = 0; - Uint64 programLifetimeId = 0; - Uint32 programVersion = 0; - const void* vao = nullptr; - // Same rule as VaoDrawMemo::vaoLifetimeId: (address, config version) is not an - // identity, because a recycled address can arrive carrying a config version - // the dead VAO also had (two mutations to configure one attribute is the - // common shape), and "the VAO did not move" would then skip the layout - // re-resolve for a different VAO. - Uint64 vaoLifetimeId = 0; - // P2 D12.4: the handle arm's answer to the same question, and one compare rather - // than the pair above. Kept BESIDE them rather than replacing them because the - // pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot - // is a value struct, not a wire type. - MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle; - Uint32 vaoConfigVersion = 0; - const void* drawFbo = nullptr; - // Never-reused lifetime id beside the raw pointer + Uint16 version: a - // deleted FBO recycled at the same address with the same fresh version - // count would otherwise compare equal (same ABA as the render-pass - // manager's fast-path memo). - Uint64 drawFboLifetimeId = 0; - Uint16 fboVersion = 0; - Bool drawFboIsDefault = false; - Uint renderStateVersion = 0; - Uint64 bindGeneration = 0; - Uint32 baseTransformFlags = 0; - Uint32 resolvedTransformFlags = 0; - // What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken - // from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects` - // already separates indexed from non-indexed draws, but not one index TYPE from - // another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT - // makes those two draws want different pipelines. - Bool primitiveRestartEnable = false; - Uint64 renderPassHash = 0; - Uint32 imageIndex = 0; - Uint64 textureEraseEpoch = 0; - Uint64 textureImageEpoch = 0; - Uint64 renderbufferImageEpoch = 0; - Uint64 sampledContentSum = 0; - Uint64 sampledParamsSum = 0; - // Guards the sampler-descriptor reuse hint: bumped by any sampler-object - // parameter or texture shape change (see GetSamplingResolutionGeneration), - // none of which the sums above cover. - Uint64 samplingResolutionGeneration = 0; - // Render-pass flavor input (DepthTest || StencilTest at snapshot time). - // A pipeline-state change that leaves this equal cannot change which - // render pass GetOrCreateRenderPass would pick, so the fast path may - // re-resolve just the pipeline against the active pass; a change that - // flips it must fall back to the full path's pass selection. - Bool drawUsesDepthStencil = false; - // The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM - // (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both - // of which the programLifetimeId/programVersion guards above already pin - carried - // here so the fast path does not re-fetch the program object to re-derive it. - Uint32 viewportCount = 1; - IntVec2 renderPassExtent = {0, 0}; - // colorAttachmentCount of the snapshotting draw's render pass: the - // pipeline-state hash input, so the fast path can refresh that hash and - // probe the pipeline memo after a state change without re-fetching the - // render-pass entry (the pass itself is pinned by renderPassHash above). - Uint32 renderPassColorCount = 0; - // Pinned with the colour count and for the same reason: the fast path recomputes the - // pipeline-state value hash from the snapshot, and that hash reads the sample count. - VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT; - VkPipeline pipeline = VK_NULL_HANDLE; - // layoutHash of the snapshotting draw's vertex-input state. The pipeline and - // the vertex-input pre-flight depend on the VAO only through this (plus the - // program, pinned separately), so a changed VAO whose aux memo carries the - // same layoutHash re-uses the snapshot's pipeline and pre-flight verdict - // outright - the VAO-cycling case Minecraft chunk rendering hits every draw. - Uint64 vaoLayoutHash = 0; - // Memoised ProgramFactory entry of the snapshotting draw, valid while - // (programLifetimeId, programVersion, resolvedTransformFlags) match - all - // checked above - AND the factory's cache structure epoch is unchanged (the - // cache is open-addressing and holds entries by value, so any insert/erase - // moves them). The fast path must re-stamp use through StampProgramUse when - // it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry. - const ProgramFactory::VkProgramObject* programObj = nullptr; - Uint64 programFactoryEpoch = 0; - // Per-entry copies of the snapshotting draw's sampled set (the scratch - // vectors below hold only the LAST full-path draw's set, which with more - // than one snapshot entry is not necessarily this entry's program). - // sampledTextures/sampledResources carry the same epoch-guarded pointer - // lifetime rules as the scratch originals: textureEraseEpoch (checked - // every probe) declines the entry before any erased resource pointer - // could be dereferenced. sampledLayouts is the layout VALUE each - // resource held when this entry's descriptors were built (the - // descriptor-reuse hint needs the SAME layout, not just a sampleable - // one), and sampledBindingRecords feeds SampledBindingsUnchanged when - // the bind generation moved. - Vector sampledTextures; - Vector sampledResources; - Vector sampledLayouts; - Vector sampledBindingRecords; - }; - // Program-keyed snapshot entries: program ping-pong (Sodium switches programs - // mid-frame every few draws) would otherwise evict the single snapshot on - // every switch and send every draw through the full path. Entries are found - // by programLifetimeId (MRU-first probe); every other guard stays per-probe, - // so a stale entry declines itself exactly like the old single snapshot did. - static constexpr Uint32 kSetupDrawSnapshotCount = 4; - SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount]; - Uint32 m_setupDrawSnapshotMru = 0; // last entry that hit or was filled - Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live - void InvalidateSetupDrawSnapshots() { - for (auto& snapshot : m_setupDrawSnapshots) { - snapshot.valid = false; - } - } - - // Per-draw scratch buffers (clear keeps capacity) — these paths run for every - // draw call and must not allocate. - Vector m_sampledTexturesScratch; - // Per-binding (texture, effective sampler) lifetime-id records from the same - // CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast - // path shadow-compares against them (SampledBindingsUnchanged) when the - // texture bind generation moved, so a redundant glBindSampler/glBindTexture - // storm that resolves to the same bindings keeps the fast path. - Vector m_sampledBindingRecordsScratch; - // Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's - // first sampled-texture loop: the resolved backend resources, so the - // post-transition loop can skip re-resolving textures whose layout is - // already sampleable. - Vector m_sampledResourcesScratch; - Vector m_storageImageTexturesScratch; - Vector m_samplerImageFeedbackScratch; - Vector m_samplerImageBindingOverridesScratch; - Vector m_vertexBuffersScratch; - Vector m_vertexOffsetsScratch; + // Per-draw scratch buffer (clear keeps capacity) - this path runs for every draw call and + // must not allocate. Vector m_patchedAttributesScratch; - Vector m_vertexConversionScratch; - Vector m_vertexRepackScratch; - struct ConvertedVertexStreamKey { - const MG_State::GLState::BufferObject* buffer = nullptr; - Uint64 changeSerial = 0; - SizeT baseOffset = 0; - Uint32 sourceStride = 0; - DataType type = DataType::Float32; - Int size = 0; - Bool normalized = false; - Bool isInteger = false; - VertexInputStateFactory::VertexStreamConversion conversion = - VertexInputStateFactory::VertexStreamConversion::None; - - Bool operator==(const ConvertedVertexStreamKey& other) const { - return buffer == other.buffer && changeSerial == other.changeSerial && - baseOffset == other.baseOffset && sourceStride == other.sourceStride && - type == other.type && size == other.size && normalized == other.normalized && - isInteger == other.isInteger && conversion == other.conversion; - } - }; - - struct ConvertedVertexStreamKeyHash { - SizeT operator()(const ConvertedVertexStreamKey& key) const { - SizeT hash = std::hash{}(key.buffer); - auto combine = [&hash](SizeT value) { - hash ^= value + static_cast(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2); - }; - combine(std::hash{}(key.changeSerial)); - combine(std::hash{}(key.baseOffset)); - combine(std::hash{}(key.sourceStride)); - combine(std::hash{}(static_cast(key.type))); - combine(std::hash{}(key.size)); - combine(std::hash{}(key.normalized)); - combine(std::hash{}(key.isInteger)); - combine(std::hash{}(static_cast(key.conversion))); - return hash; - } - }; - - struct ConvertedVertexStream { - BufferSlice slice; - // Number of source elements the cached slice covers. A draw needing a prefix of - // this range reuses the slice (converted streams are tightly packed); a draw - // needing more reconverts and replaces the entry, so per (buffer, layout) a - // frame converts at most the largest range any draw asked for. - SizeT elementCount = 0; - // Pins the source buffer for the frame so its heap address cannot be reused by - // a new BufferObject while this pointer-keyed entry is alive. - SharedPtr sourcePin; - }; - UnorderedMap - m_convertedVertexStreams; - - // One VAO's resolved vkCmdBindVertexBuffers arguments, reusable by a later draw - // that would resolve them to the same thing. Consecutive draws in a chunk-renderer - // frame keep the program and the vertex layout and only swap the VAO, so a - // per-VAO memo turns the second and later draws through each VAO into a validate - // plus (usually skipped) rebind. - // - // Only whole-buffer bindings are memoised. Client-memory and format-converted - // streams re-upload from a range that depends on the draw's own vertex/index - // range, and synthetic bindings carry glVertexAttrib* values that are not part - // of any key here; a layout using any of them is never stored. - // Field order is hit-path cache locality, hot to cold: the per-draw validate - // reads the scalars and the EBO memo head, then only the first bindingCount - // elements of vkBuffers/vkOffsets; the per-binding revalidation arrays at the - // tail are touched once per frame at most. - struct ResolvedVertexBindings { - // Must equal DynamicStateShadow::kMaxShadowedVertexBindings (static_assert in - // the .cpp): past that width the bind shadow cannot skip a redundant bind - // either, so a wider layout resolves per draw. Minecraft-shaped layouts use four. - static constexpr Uint32 kMaxBindings = 8; - - // Frame serial of the last completed resolve OR cross-frame revalidation. - // Zero until a resolve completes, and reset to zero before one starts, so a - // resolve that bails out midway cannot leave a half-filled entry matchable. - // Unlike the original frame-scoped memo, an entry whose buffers are all - // resident and unmapped is revalidated across frames (per-binding slice - // epoch compares) instead of re-resolved - see TryBindResolvedVertexBindings. - Uint64 frameSerial = 0; - // Identity of the resolved Vulkan layout: the VAO's content hash - // (VertexInputStateFactory::GetOrComputeHash - the same value the factory - // keys its entries on) fixes bindings.size(), each binding's base offset, - // which bindings are client/converted, and (through the mixed-in buffer - // addresses) which buffer each binding reads. Compared against the VAO's - // own hash memo on the hit path, so a hit never touches the factory entry. - VertexInputStateFactory::HashType vertexInputHash = 0; - // The program's vertex input layout: decides the synthetic-binding set and - // hence the total binding count. - Uint32 activeAttribMask = 0; - Uint32 bindingCount = 0; - // VkBufferManager::GetSliceEpochCounter() at resolve time. Still equal means - // no buffer anywhere changed its slice or was persistently mapped since, which - // settles every per-binding question below in one compare. - Uint64 sliceEpochCounter = 0; - // Any bound buffer already carrying a host map when the slice was resolved. - // Such a buffer can mutate its shadow with no API call, so it has to be - // re-pushed per draw and the one-compare path above cannot apply. - Bool anyBufferMapped = true; - - // Resident element-buffer slice memo (skips the per-draw AcquireResidentSlice - // for the VAO's EBO, which cold-chases 500+ distinct resources in a - // chunk-cycling frame). Self-validating exactly like the bindings above: a hit - // requires the LIVE bound EBO pointer to equal indexBuffer AND either an - // unmoved manager-wide slice-epoch counter (nothing anywhere changed slices - // or gained a host map, the same one-compare rescue the vertex half uses) or - // that buffer's resource still carrying indexSliceEpoch (epochs are minted - // from a process-lifetime counter, so a recycled address can never - // revalidate). Restart-substituted and streamed EBOs are never stored. - // indexFrameSerial tracks the last frame the resource's GPU-use serial was - // stamped through this memo; 0 means no index memo. Independent of the - // vertex half: both are (pointer, epoch)-validated, so neither can serve - // stale state for the other. - const MG_State::GLState::BufferObject* indexBuffer = nullptr; - Uint64 indexSliceEpoch = 0; - // GetSliceEpochCounter() when the resource's epoch was last verified; only - // meaningful while indexFrameSerial matches the current frame serial. - Uint64 indexSliceEpochCounter = 0; - VkBuffer indexVkBuffer = VK_NULL_HANDLE; - VkDeviceSize indexSliceOffset = 0; - Uint64 indexFrameSerial = 0; - // The EBO carried a host map when the slice was recorded - the mirror of - // anyBufferMapped on the vertex half. A shadow-backed (non-adopted) - // persistent map mutates its shadow with no API call and no epoch bump, so - // the one-compare rescue must decline and re-run the acquire, whose - // SyncPersistentMappedRange is the push-down. A map taken AFTER the record - // is already covered: AcquirePersistentMap bumps the slice epoch for the - // request itself, adopted or declined. - Bool indexBufferMapped = false; - - // Bound per draw (first bindingCount elements). - VkBuffer vkBuffers[kMaxBindings] = {}; - VkDeviceSize vkOffsets[kMaxBindings] = {}; - // Per binding: the VAO attribute location its buffer comes from, that buffer, - // and the buffer's VkBufferManager slice epoch when the slice was resolved. - // Only read by the per-frame revalidation and the something-moved fallback. - Uint8 attributeLocations[kMaxBindings] = {}; - const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {}; - Uint64 sliceEpochs[kMaxBindings] = {}; - }; - // One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to - // the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone - // only picks the slot, and the never-reused lifetime id is what proves the slot - // is THIS VAO's, so the successor allocated onto a destroyed VAO's address - // always misses. That identity check is load-bearing and the content-hash - // validations below do NOT stand in for it - a recycled address under a - // byte-identical configuration reproduces the content hash exactly, which is - // how a destroyed VAO's resolved bindings were once handed to its successor's - // draw. The slot is still never dereferenced through vaoKey, and every fact it - // carries is still validated against live state before use: - // - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE - // VAO's own hash memo (which the VAO's config version guards), so a config - // change or a buffer rebind misses even for the same object. - // - bindings revalidates per draw exactly as before (frame serial, content - // hash, per-binding live buffer pointers and slice epochs). - struct alignas(64) VaoDrawMemo { - // P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an - // identity, so the pointer-plus-lifetime-id pair below stops being a key here; - // the slot also picks the table entry, so the address hash and the two-way probe - // go with it. Null in an entry that has never been claimed. - MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle; - const MG_State::GLState::VertexArrayObject* vaoKey = nullptr; - // The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer - // ALONE is not an identity: a deleted VAO's heap address is handed straight - // back by the next glGenVertexArrays-shaped allocation, and the successor then - // matched this slot and inherited the dead object's memos. Both stated - // defences failed with it, because both reduce to the content hash and the - // content hash's buffer-identity component was itself a recycled heap address. - Uint64 vaoLifetimeId = 0; - // The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two - // layout facts below were derived from; 0 while nothing valid is stored. - Uint64 contentHash = 0; - Bool layoutFactsValid = false; - // The resolved layout identity + packed (unsupported, location) masks - - // the exact values GetBackendAuxMemo used to serve, moved here so the - // per-draw probe stays inside this table's one hot line instead of - // touching a second cold line of every cycled VAO object. - Uint64 layoutHash = 0; - Uint64 layoutAuxMasks = 0; - ResolvedVertexBindings bindings; - }; - // Fixed-size, allocated on first use, never rehashed or swept: entries are - // recycled in place on slot collisions (two-slot probe, older frame serial - // evicted), and stale entries self-invalidate through the compares above. A - // fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer - // stable for the duration of a draw, which the EBO memo handoff - // (m_currentDrawResolvedEntry) relies on. - // - // [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand - // Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim - // rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead - // of a hashed heap address plus a lifetime id). Two reasons, and the second is the - // whole of review v2's MAJOR 1: - // * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this - // table with the live VAO set is megabytes on a platform with an LMK, where the - // other two memos are 48 B and can afford it; - // * this is the ONLY one of the three memos that had a capacity before this package. - // Losing an entry here costs a vertex-binding re-resolve, exactly what losing it - // cost on the base ref, so at any working-set size this table is no worse than what - // it replaces - and strictly better below capacity, where the handle is a bijection - // with the slot and the two-way probe never collides at all. The other two memos - // (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none. - static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two - Vector m_vaoDrawMemoTable; - // The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both - // derive the same handle for the same VAO. Per renderer, never a process-global: a - // global would share one table and one reclamation clock across two live contexts and - // outlive every one of them (review v2 minor 4). - MagmaPipeIdentityTables m_pipeIdentity; - // The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's - // first, so there is no second memo in front of it here. - MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) { - return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, - vao.GetLifetimeId()); - } - // "Is this VAO's content hash already memoized?", asked of whichever side owns the - // memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL - // build's three readers keep compiling to the very same two loads they always did - - // G1 admits no resize, and an out-of-line call here would be one. - [[gnu::always_inline]] inline Bool VaoContentHashIfKnown( - const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const { - return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash); - } - // Finds the slot holding `vao`, or recycles the older of its two candidate - // slots into an empty memo keyed on `vao`. Never returns null. - VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao); - // The current draw's memo entry, set by UploadAndBindVertexBuffers and consumed - // by the same draw's UploadAndBindIndexBuffer (the EBO memo lives in the same - // entry). Valid ONLY within that window: the next draw's lookup can recycle the - // slot. Null when the draw's layout is not memoisable. - ResolvedVertexBindings* m_currentDrawResolvedEntry = nullptr; void CreateInstance(); VkResult SetupDebugMessenger(); @@ -1995,31 +1560,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool ResolvePrimitiveRestartEnable(Flags aspects, const IndexBufferView* pIndexBufferView) const; - VkPipeline GetOrCreatePipeline( - GLenum mode, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - ProgramFactory::CompileOptionFlags transformFlags, - const MG_State::GLState::VertexArrayObject& vao, - const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); void DestroyComputePipelines(); - // Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to - // flush the pending recording (see the body), which retires the current command buffer. - Bool PrepareStorageImageTextures( - FrameContext::FrameData& frame, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj); - // Vulkan forbids a sampled descriptor and writable storage descriptor from naming the - // same image subresource in one shader operation. Snapshot only the sampler side; the - // storage descriptor continues to name the application texture. - Bool PrepareSamplerImageFeedbackSnapshots( - FrameContext::FrameData& frame, - const MagmaProgramSource& program, - const ProgramFactory::VkProgramObject& programObj, - VkPipelineStageFlags consumerShaderStageMask); - // The per-draw dynamic-state tail (viewport, scissor, blend constants, depth // bias, line width, stencil), gated behind one render-state-parameters-version // compare per command buffer - see the gate fields in DynamicStateShadow. @@ -2040,23 +1582,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u; } - Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, - const ProgramFactory::VkProgramObject& programObj, - const DrawCmdParam& drawParams, - const IndexBufferView* pIndexBufferView); - // Binds `entry`'s memoised buffers when every input it was resolved from is - // still live and unchanged, else returns false and leaves nothing bound. - // vaoContentHash is the VAO's memoised content hash (GetBackendHashMemo), which - // pins the layout AND the bound buffers without resolving the factory entry. - // Non-const entry: a cross-frame revalidation refreshes its serial/epoch stamps. - Bool TryBindResolvedVertexBindings(VkCommandBuffer commandBuffer, - const MG_State::GLState::VertexArrayObject& vao, - ResolvedVertexBindings& entry, - Uint64 vaoContentHash, - Uint32 activeAttribMask, Uint64 frameSerial); - Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame, - const MG_State::GLState::VertexArrayObject& vao, - const IndexBufferView* pIndexBufferView = nullptr); Bool InitializeBlitResources(); Bool InitializeDepthMipmapResources(); void ShutdownBlitResources(); @@ -2123,14 +1648,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool MaterializePendingDepthStencilClearForDefaultFramebuffer( VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment, const ClearAttachmentPayload& payload); - Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, - MG_State::GLState::ITextureObject& texture, - VkTextureManager::TextureResource& resource, - Uint32 baseMipLevel, - Uint32 generateMipLevelCount, - const IntVec3& storageBaseTexelSize, - VkImageLayout originalLayout, - VkImageLayout finalLayout); Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame); public: diff --git a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp index eb206f68..b317e2eb 100644 --- a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp +++ b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp @@ -6,34 +6,10 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// Magma's {slot, gen} mint (MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h) and the claim -// rule its per-slot memo tables use, at the one point HandleRecycleScenario cannot reach: a -// REAL SLOT REUSE. -// -// WHY THIS SUITE EXISTS (fix-aba review v1, MAJOR 1). The AbaControl integration lanes defeat -// the object identity that SELECTS the slot, and that is the whole of what a same-frame pixel -// reproducer can defeat: -// -// * the mint has no death notification, so a slot returns to the free list only through -// OnFrameBoundary's age sweep (kSweepInterval 256, kRetireAgeBoundaries 1024); -// * HandleRecycleScenario issues five frame boundaries, so its replacement VAO gets a -// BRAND-NEW slot at Gen 1 and the generation never participates in a compare; -// * a real reuse needs >= 1024 idle boundaries, which puts the two draws in different frames -// - and ResolvedVertexBindings, the only memo carrying a GPU slice rather than a layout, -// declines across frames by design. -// -// So deleting the `++m_entries[index].Gen` in MagmaPipeIdentityTable::ClaimSlot leaves every -// arm of HandleRecycleScenario green. It reds AnIdleSlotIsRetiredAndReusedWithANewGeneration -// and AReusedSlotDoesNotServeItsPredecessorsMemo below, which is the whole point of the file. -// -// The suite lives beside SlotAllocatorTest because it asserts the same identity contract that -// file asserts for the client allocator - "Gen moves on REUSE and never on respecify" - for the -// second mint in the tree, the one Magma keeps because nothing in P2 can call the client -// allocator's Free (MagmaPipeArms.h says why). It needs no GL context, no driver and no Vulkan -// loader: MagmaPipeArms.h is header-only. -// -// Push-only, like everything it tests, so every case is a visible SKIP in a pull build rather -// than a vanishing test (G2 name parity). +// Magma's server-side program source and run-ahead capability, the parts of this suite that +// outlived the {slot, gen} mint. P13 W6 deleted MagmaPipeIdentityTables and the claim rule +// with the frontend arm's VAO memos they keyed (the record arm keys every object on the +// applier's own {slot, gen}); their four cases went with them (docs/ci/ci-cleanup-log.md). #include #include "Includes.h" @@ -41,7 +17,6 @@ #include #include -#include #if MOBILEGL_BUILD_DISAGGREGATED #include #endif @@ -52,158 +27,10 @@ namespace { using namespace MobileGL::MG_Backend::DirectVulkan; using MG_Pipe::MGPipeHandle; - // What VertexInputStateFactory::VaoBackendMemos is, reduced to the two fields the claim - // rule needs: the Owner it compares, and one payload word standing in for the memo's - // contents (there, the content hash and the resolved-entry pointer). The RULE is production - // code - MagmaPipeClaimSlotMemos - not a copy of it. - struct TestMemos { - MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle; - Uint64 Payload = 0; - }; - - // MagmaPipeIdentityTable's sweep cadence and retirement age are private, so the number of - // boundaries needed to retire an object last used at boundary 0 is spelled out here: the - // sweep runs when (boundary % 256) == 0 and retires entries idle for more than 1024 - // boundaries, so the first sweep that can retire it is boundary 1280. Every case that - // depends on this ASSERTs the retire actually happened, so a change to either constant - // fails loudly instead of silently turning these cases into "two unrelated objects". - constexpr int kBoundariesToRetireAnObjectIdleSinceTheStart = 1280; - - class MagmaPipeIdentityTest : public ::testing::Test { - protected: - void SetUp() override { - m_savedKnob = MG_Config::Features.PipeHandleAbaControl; - MG_Config::Features.PipeHandleAbaControl = false; - } - void TearDown() override { MG_Config::Features.PipeHandleAbaControl = m_savedKnob; } - - // Ages the mint far enough for the sweep to retire everything that has been idle since - // the start, while touching `keepAliveLifetimeId` on every boundary so that IT is never - // retired. The keep-alive is what makes the reuse non-degenerate: it holds the first - // allocatable slot, so the slot under test is not the one negative control C aliases - // everything onto (kMagmaPipeAbaControlSlotIndex). - static void AgeUntilTheSweepRetiresTheIdleSlots(MagmaPipeIdentityTable& mint, - Uint64 keepAliveLifetimeId) { - for (int i = 0; i < kBoundariesToRetireAnObjectIdleSinceTheStart; ++i) { - mint.Acquire(keepAliveLifetimeId); - mint.OnFrameBoundary(); - } - } - - Bool m_savedKnob = false; - }; - - // The precondition every case below rests on, asserted on its own so that a failure here - // reads as "the mint stopped reusing slots" rather than as a memo bug. - TEST_F(MagmaPipeIdentityTest, AnIdleSlotIsRetiredAndReusedWithANewGeneration) { - MagmaPipeIdentityTable mint("VertexElementsCso"); - const MGPipeHandle keepAlive = mint.Acquire(1); - const MGPipeHandle first = mint.Acquire(2); - ASSERT_EQ(keepAlive.Slot, MG_Pipe::kMGPipeFirstAllocatableSlot); - ASSERT_NE(first.Slot, keepAlive.Slot); - ASSERT_EQ(first.Gen, 1u) << "a slot's first handout is generation 1"; - ASSERT_EQ(mint.LiveCount(), 2u); - - AgeUntilTheSweepRetiresTheIdleSlots(mint, 1); - ASSERT_EQ(mint.LiveCount(), 1u) - << "the idle slot was not retired, so nothing in this file is a slot REUSE"; - - // The step HandleRecycleScenario cannot take. With an empty free list this would be a - // brand-new slot at Gen 1 and the generation would never participate in any compare - - // which is exactly what the scenario measures (redVao slot=2 gen=1, greenVao slot=3 - // gen=1) and why it cannot catch a deleted ++Gen. - const MGPipeHandle second = mint.Acquire(3); - EXPECT_EQ(second.Slot, first.Slot) << "a retired slot must come back before the high-water mark"; - EXPECT_EQ(second.Gen, first.Gen + 1u) << "a slot that changes owner must change generation"; - EXPECT_FALSE(second == first); - EXPECT_EQ(mint.Count(), 2u) << "the reuse must not mint a third slot"; - } - - // THE CASE THE ++Gen IS LOAD-BEARING FOR. Knob off: the replacement gets the predecessor's - // SLOT, so the slot cannot be what separates them - only the generation can. - TEST_F(MagmaPipeIdentityTest, AReusedSlotDoesNotServeItsPredecessorsMemo) { - MagmaPipeIdentityTable mint("VertexElementsCso"); - MagmaPipeSlotTable memos; - - mint.Acquire(1); // the keep-alive, so the slot under test is not slot index 0 - const MGPipeHandle first = mint.Acquire(2); - MagmaPipeClaimSlotMemos(memos, first).Payload = 0xDEADull; - ASSERT_TRUE(MagmaPipeClaimSlotMemos(memos, first).Owner == first); - ASSERT_EQ(MagmaPipeClaimSlotMemos(memos, first).Payload, 0xDEADull); - - AgeUntilTheSweepRetiresTheIdleSlots(mint, 1); - const MGPipeHandle second = mint.Acquire(3); - ASSERT_EQ(second.Slot, first.Slot) << "not a slot reuse, so this case would prove nothing"; - // EXPECT, not ASSERT: with the generation frozen the memo assertion below is exactly what - // goes red, and a reader of the failure should see both halves rather than stop here. - EXPECT_NE(second.Gen, first.Gen); - - const TestMemos& served = MagmaPipeClaimSlotMemos(memos, second); - EXPECT_TRUE(served.Owner == second) << "the entry was not claimed for its new owner"; - EXPECT_EQ(served.Payload, 0ull) - << "the replacement inherited the dead object's memo out of the SAME slot: the " - "generation is the only thing that separates {slot, gen=N} from {slot, gen=N+1}, " - "and it did not"; - } - - // The same shape with negative control C on, which is what makes the case above a control - // rather than a tautology: with the knob on the memo IS served across the generation. - // - // The knob is set before the first claim, as a process-wide knob is in a real run: what it - // defeats is the identity that selects the entry, so a run that stamps with it off and reads - // with it on would be reading a different entry, not an aliased one. - TEST_F(MagmaPipeIdentityTest, TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot) { - MG_Config::Features.PipeHandleAbaControl = true; - - MagmaPipeIdentityTable mint("VertexElementsCso"); - MagmaPipeSlotTable memos; - - mint.Acquire(1); - const MGPipeHandle first = mint.Acquire(2); - TestMemos& stamped = MagmaPipeClaimSlotMemos(memos, first); - stamped.Payload = 0xDEADull; - - AgeUntilTheSweepRetiresTheIdleSlots(mint, 1); - const MGPipeHandle second = mint.Acquire(3); - ASSERT_EQ(second.Slot, first.Slot); - EXPECT_NE(second.Gen, first.Gen); - - const TestMemos& served = MagmaPipeClaimSlotMemos(memos, second); - EXPECT_EQ(&served, &stamped) << "the control must collapse every object onto one entry"; - EXPECT_EQ(served.Payload, 0xDEADull) - << "negative control C is not defeating the claim rule any more: the replacement was " - "NOT handed its predecessor's memo, so the AbaControl lanes assert nothing"; - EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(served.Owner)) - << "the control hands the entry back UNCLEARED and UNCLAIMED - it never learns whose " - "it is, which is what 'replace the identity with a constant' means"; - } - - // The other half of the {slot, gen} contract, and the reason a deleted ++Gen cannot be - // 'fixed' by bumping Gen on every acquisition: a live object keeps its handle across - // sweeps, so its memo survives a reconfiguration instead of being recomputed per draw. - TEST_F(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo) { - MagmaPipeIdentityTable mint("VertexElementsCso"); - MagmaPipeSlotTable memos; - - const MGPipeHandle handle = mint.Acquire(2); - MagmaPipeClaimSlotMemos(memos, handle).Payload = 0xBEEFull; - - // Past two sweeps (256 and 512), drawn on every boundary. - for (int i = 0; i < 700; ++i) { - mint.Acquire(2); - mint.OnFrameBoundary(); - } - const MGPipeHandle again = mint.Acquire(2); - EXPECT_TRUE(again == handle) << "a live object's handle moved under the age sweep"; - EXPECT_EQ(MagmaPipeClaimSlotMemos(memos, again).Payload, 0xBEEFull) - << "a live object's memo was cleared without its slot changing owner"; - EXPECT_EQ(mint.Count(), 1u); - } - // Historical test name retained. Each backend's readiness is now a real gate: // false forbids the capability, true permits it. Production keeps its own // Magma readiness constant, which the bootstrap and GPU queue tests also check. - TEST_F(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) { + TEST(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) { EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false), 0u); EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true), static_cast(MG_Pipe::kCapRunAheadApply)); @@ -244,8 +71,6 @@ namespace { record.GlobalConstants = {5, 6}; record.GlobalConstantsVersion = 11; const MagmaProgramSource source({7, 2}, record); - EXPECT_TRUE(source.IsWire()); - EXPECT_EQ(source.Frontend(), nullptr); EXPECT_EQ(source.GetUniformBlockBinding(0), 3u); EXPECT_EQ(source.GetUniformBlockName(0), "Block"); EXPECT_EQ(source.GetUBOSizeAt(0), 16u);