From 57aeeec0537d47941827e95b6d8dece3aa2bdbcf Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 6 Aug 2026 06:24:37 -0400 Subject: [PATCH] [Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known A per-draw CPU profile of a real Minecraft frame (perf on the render thread, which sits at 100% of one core on both backends) said the deficit is translation overhead, not the GPU, and named where it goes. This removes the largest items it found, on both backends and in the shared frontend they both feed. The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread called eglGetCurrentContext on every invocation, and glvnd answers that with a getpid() fork check - a real syscall. The predicate sits two and three deep in every draw (the deferred-release drain, the global-UBO ring availability check, and the ring allocation), so it accounted for 16.3% of the render thread. EGL is still the ground truth, but re-verifying it once per thread per frame catches an external migration at the next frame boundary rather than the next call, which recovers the same bookkeeping. Texture uploads now carry a dirty region instead of a per-level flag. Minecraft animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas and respecifies the lightmap every frame; a per-level flag turned each of those into a full-level re-upload - about 3.6 MB a frame of texels nobody changed. MipmapStorage accumulates the written box, Espryt uploads it with UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box. The box is a union, not a range list: repeated writes to one level widen it and it degrades to exactly the old whole-level upload, which is the honest worst case. glBufferData(NULL) is the orphaning idiom, and the backend was answering it by uploading the stale CPU shadow - turning a rename the driver does for free into a full synchronized upload. BufferObject now records that a NULL respecify leaves the store undefined, and the upload is skipped until content is actually written. The rest are smaller and of a kind: the deferred-release queue is probed without taking its mutex, the UBO ring waits on the frame fence that frees the space it needs instead of draining the whole pipeline with glFinish at the size cap, VAO binds go through a shadow so a draw's second bind of the same object does not reach the driver, the per-draw clean-texture probe short-circuits on the content version before rebuilding shape info, glUniform drops byte-identical writes (which otherwise dirty the whole UBO for the next draw), re-binding the texture or VAO a slot already holds no longer bumps the generation counters a backend fast path is keyed on, and the texture validators stopped taking shared_ptr by value. On Magma: descriptor-set reuse keeps four entries instead of one, because draws alternating between two programs - the chunk/entity ping-pong - thrashed a single slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose contents survive two frame boundaries is promoted to resident storage instead of being re-copied into the per-frame arena forever; and sampled-read barriers name only the shader stages whose device feature is enabled, which also removes a latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a device need not have). Measured with the Minecraft rig (render distance 32, p50 fps, same machine, single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6; 26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma (854 -> 766) with the native baseline itself moving 838 -> 1031 between the two sessions, so treat that cell as unresolved rather than a regression measured. Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are the whole of the evidence, and a conformance regression would not have been caught here. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 55 +++++- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 5 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 178 ++++++++++++++---- MobileGL/MG_Backend/DirectGLES/Managers.h | 13 ++ .../DirectVulkan/Renderer/UniformManager.cpp | 43 +++-- .../DirectVulkan/Renderer/UniformManager.h | 24 ++- .../DirectVulkan/Renderer/VkBufferManager.cpp | 28 +++ .../DirectVulkan/Renderer/VkBufferManager.h | 11 ++ .../Renderer/VkTextureManager.cpp | 85 ++++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 6 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 18 +- .../DirectVulkan/Renderer/VulkanRenderer.h | 7 + .../MG_Impl/GLImpl/Program/GL_Program.cpp | 8 + .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 24 ++- .../MG_Impl/GLImpl/Texture/Validators.cpp | 6 +- MobileGL/MG_Impl/GLImpl/Texture/Validators.h | 6 +- .../GLState/BufferState/BufferObject.cpp | 12 ++ .../GLState/BufferState/BufferObject.h | 7 + MobileGL/MG_State/GLState/Core.h | 4 +- .../GLState/TextureState/MipmapStorage.cpp | 54 +++++- .../GLState/TextureState/MipmapStorage.h | 25 +++ .../TextureState/MipmapUploadTargetArray.h | 10 + .../GLState/TextureState/TextureObject.cpp | 12 ++ .../GLState/TextureState/TextureObject.h | 17 ++ .../TextureState/TextureObject2DCube.cpp | 12 ++ .../TextureState/TextureObject2DCube.h | 4 + .../GLState/TextureState/TextureState.h | 7 +- .../VertexArrayState/VertexArrayState.cpp | 6 +- MobileGL/MG_Util/Types.h | 7 +- 29 files changed, 605 insertions(+), 89 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7a9b0896..e93c95ad 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1518,7 +1518,7 @@ namespace MobileGL::MG_Backend::DirectGLES { backendVAOIt->second->Bind(); } } else { - g_GLESFuncs.glBindVertexArray(0); + VertexArrayImpl::BindBackendVAOId(0); } } @@ -3001,7 +3001,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const Float uvTransform[4] = {mirrorX ? -uvScaleX : uvScaleX, mirrorY ? -uvScaleY : uvScaleY, mirrorX ? uvScaleX : 0.0f, mirrorY ? uvScaleY : 0.0f}; - g_GLESFuncs.glBindVertexArray(s_vertexArray); + VertexArrayImpl::BindBackendVAOId(s_vertexArray); g_GLESFuncs.glActiveTexture(GL_TEXTURE0); g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, s_texture); g_GLESFuncs.glViewport(dstLeft, dstBottom, dstWidth, dstHeight); @@ -3060,7 +3060,7 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glUseProgram(static_cast(previousProgram)); g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, static_cast(previousTexture)); g_GLESFuncs.glActiveTexture(static_cast(previousActiveTexture)); - g_GLESFuncs.glBindVertexArray(static_cast(previousVertexArray)); + VertexArrayImpl::BindBackendVAOId(static_cast(previousVertexArray)); g_GLESFuncs.glViewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]); g_GLESFuncs.glScissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3]); @@ -5919,6 +5919,15 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + namespace { + // EGL ground-truth verification stamp, per thread. glvnd's + // eglGetCurrentContext performs fork detection with a real getpid() + // syscall on every call, and this predicate sits 2-3 deep in every + // draw - measured at 16% of the render thread on a live workload. + thread_local Uint64 t_eglVerifiedFrameSerial = ~0ull; + thread_local Uint t_eglVerifiedContextGeneration = 0; + } // namespace + Bool IsBackendContextCurrentOnThisThread() { if (g_Context == EGL_NO_CONTEXT) { return false; @@ -5929,10 +5938,20 @@ namespace MobileGL::MG_Backend::DirectGLES { // Belt and braces: EGL itself is the ground truth. A migration that bypassed // MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim // standing, or GL calls would silently no-op while shadow bookkeeping (bind - // cache, synced serials) still advances. + // cache, synced serials) still advances. Re-verify once per (thread, frame, + // context generation) rather than per call: an external migration is caught + // at the next frame boundary instead of the next call, which recovers the + // bookkeeping just the same, without paying a syscall on every draw. + const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed); + if (t_eglVerifiedFrameSerial == frameSerial && + t_eglVerifiedContextGeneration == g_syncContextGeneration) { + return true; + } if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != g_Context) { return false; } + t_eglVerifiedFrameSerial = frameSerial; + t_eglVerifiedContextGeneration = g_syncContextGeneration; return true; } @@ -6227,6 +6246,33 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); } Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); } + Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) { + if (CompletedFrameSerial() >= serial) return true; + if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false; + // Fences signal in submission order, so the live fence with the SMALLEST + // serial at or past the target is the earliest event that proves the + // target frame retired. A recycled slot (GPU more than ring-depth frames + // behind) leaves no usable fence; report failure and let the caller pick + // its own fallback rather than draining the whole queue here. + FrameFence* best = nullptr; + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; + if (slot.serial < serial) continue; + if (!best || slot.serial < best->serial) best = &slot; + } + if (!best) return false; + const GLenum status = + g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs); + if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false; + Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed); + if (best->serial > completed) { + g_completedFrameSerial.store(best->serial, std::memory_order_relaxed); + } + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync); + best->sync = nullptr; + return true; + } + void Present() { // Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush // makes it reachable), then non-blocking-poll prior frames' fences AFTER to @@ -6272,6 +6318,7 @@ namespace MobileGL::MG_Backend::DirectGLES { XfbImpl::OnBackendContextDestroyed(); ScratchFBOImpl::OnBackendContextDestroyed(); FramebufferImpl::InvalidateFramebufferBindingCache(); + VertexArrayImpl::InvalidateVAOBindingCache(); PixelStoreImpl::InvalidatePackStateCache(); // Texture ids belong to the dying context; wrappers destroyed later must // not glDeleteTextures a recycled name in a successor context. diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index bc732f6c..2b9870e7 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -160,6 +160,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N. Uint64 CurrentFrameSerial(); Uint64 CompletedFrameSerial(); + // Block (up to timeoutNs) until the given frame serial provably retired on the + // GPU, using the per-frame fence ring. False when no usable fence covers the + // serial (fence-less context, foreign thread, or the slot was recycled); + // completion state is untouched in that case. + Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs); // Applies (or defers until the window surface exists) the app-requested // eglSwapInterval on the native EGL surface. void SetSwapInterval(Int interval); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 7feae4f3..401e7d32 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -292,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // sync point with a current ES context. Vector> g_deferredBufferReleases; std::mutex g_deferredBufferReleasesMutex; + // Cheap emptiness probe so the per-draw drain can skip the mutex and + // context check when nothing was enqueued (the overwhelmingly common + // case). Written only under the mutex; read lock-free. + std::atomic g_hasDeferredBufferReleases{false}; // --- Buffer-storage pool (Mesa-style BO recycle) ------------------------- // Recycle idle GL buffer ids of an EXACT byte size instead of glDeleteBuffers @@ -457,8 +461,14 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT size = bufferObject.GetSize(); const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage()); BindBufferId(TempBufferTarget, resource.id); - g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, - size > 0 ? bufferObject.MappedData() : nullptr, usage); + // An orphaning respecify (glBufferData with NULL, content never + // written since) stays a pure NULL reallocation: the driver renames + // the store without a stall and nothing is transferred. Uploading + // the stale shadow here turned Minecraft-style orphaning into a + // full-size synchronized upload. + const void* initialData = + (size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr; + g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage); resource.storageSize = size; resource.storageInitialized = true; resource.pendingRespecify = false; @@ -680,6 +690,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } const std::lock_guard lock(g_deferredBufferReleasesMutex); g_deferredBufferReleases.push_back(std::move(resource)); + g_hasDeferredBufferReleases.store(true, std::memory_order_release); } const BufferBackendOps g_glesBufferBackendOps = { @@ -706,6 +717,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const std::lock_guard lock(g_deferredBufferReleasesMutex); // The ES context owning these ids is going away; just drop the handles. g_deferredBufferReleases.clear(); + g_hasDeferredBufferReleases.store(false, std::memory_order_release); } void OnBackendContextDestroyed() { @@ -720,11 +732,15 @@ namespace MobileGL::MG_Backend::DirectGLES { } void ProcessDeferredBufferReleases() { + // Runs on every draw; skip the context check, mutex and vector churn + // outright when nothing was enqueued since the last drain. + if (!g_hasDeferredBufferReleases.load(std::memory_order_acquire)) return; if (!CanTouchGLNow()) return; Vector> releases; { const std::lock_guard lock(g_deferredBufferReleasesMutex); releases.swap(g_deferredBufferReleases); + g_hasDeferredBufferReleases.store(false, std::memory_order_release); } for (auto& resource : releases) { auto* glesResource = static_cast(resource.get()); @@ -1115,8 +1131,32 @@ namespace MobileGL::MG_Backend::DirectGLES { } else if (g_uboRing.creationFailed) { return false; // store lost; callers fall back to glBufferSubData } else { - // At the size cap (>kUboRingMaxBytes of uniforms in flight — not a - // real workload): drain the GPU once rather than corrupt live slots. + // At the size cap (>kUboRingMaxBytes of uniforms in flight). First + // try to free room by waiting for the OLDEST in-flight frames to + // retire - a bounded wait that ends as soon as enough tail space + // exists, instead of draining the entire queue. + constexpr Uint64 kFrameWaitNs = 50ull * 1000 * 1000; // 50ms per frame + while (!g_uboRingFrameMarks.empty() && + g_uboRing.head + alignedSize - g_uboRing.tail > g_uboRing.size) { + const auto& oldest = g_uboRingFrameMarks.front(); + if (!DirectGLES::WaitForFrameSerialCompleted(oldest.frameSerial, kFrameWaitNs)) { + break; + } + if (oldest.headAtPresent > g_uboRing.tail) g_uboRing.tail = oldest.headAtPresent; + g_uboRingFrameMarks.erase(g_uboRingFrameMarks.begin()); + } + if (g_uboRing.head + alignedSize - g_uboRing.tail <= g_uboRing.size) { + offset = static_cast(g_uboRing.head % g_uboRing.size); + if (offset + alignedSize > g_uboRing.size) { + g_uboRing.head += g_uboRing.size - offset; + offset = 0; + } + g_uboRing.head += alignedSize; + outOffset = offset; + return true; + } + // No usable fence covers the oldest frames: drain once rather than + // corrupt live slots. if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); g_uboRing.tail = g_uboRing.head; g_uboRingFrameMarks.clear(); @@ -1234,6 +1274,7 @@ namespace MobileGL::MG_Backend::DirectGLES { BackendVertexArrayObject::~BackendVertexArrayObject() { if (m_backendVAOId != 0) { + NoteVAOIdDeleted(m_backendVAOId); g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId); m_backendVAOId = 0; } @@ -1246,11 +1287,35 @@ namespace MobileGL::MG_Backend::DirectGLES { } } + namespace { + Uint g_boundBackendVAOId = 0; + Bool g_boundBackendVAOKnown = false; + } // namespace + + void BindBackendVAOId(Uint id) { + if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { + return; + } + g_GLESFuncs.glBindVertexArray(id); + g_boundBackendVAOId = id; + g_boundBackendVAOKnown = true; + } + + void InvalidateVAOBindingCache() { + g_boundBackendVAOKnown = false; + } + + void NoteVAOIdDeleted(Uint id) { + if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) { + g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0 + } + } + void BackendVertexArrayObject::Bind() const { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_GLESFuncs.glBindVertexArray(m_backendVAOId); + BindBackendVAOId(m_backendVAOId); } inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) { @@ -1792,7 +1857,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // textures thrash, forcing a real glBindTexture per texture per draw. When // nothing needs uploading, skip the bind + upload machinery entirely; // BindCurrentTextures() re-establishes the real sampling bindings regardless. - if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) { + // The content-version stamp short-circuits before any shape probing: it + // bumps on every CPU-side pixel mutation, so an unchanged stamp plus an + // unchanged shape means no level can be dirty. Shape stays a separate + // compare because a NULL-data glTexImage changes it without touching the + // content version. + if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap && + m_syncedContentVersion != 0 && + m_syncedContentVersion == stateTextureObject->GetContentVersion()) { auto* mipmapObject = static_cast(stateTextureObject.get()); const auto probeBaseSize = stateTextureObject->GetBaseSize(); @@ -1804,25 +1876,10 @@ namespace MobileGL::MG_Backend::DirectGLES { 0, stateTextureObject->GetSamples(), stateTextureObject->HasFixedSampleLocations()}; - // Equal info => needsRegeneration is false, and canAppendMipmaps is - // false too (it requires strictly more mip levels than the last sync). - // So the only remaining work would be re-uploading dirty levels. if (probe == m_prevTextureInfo) { - Bool anyDirty = false; - for (const auto& uploadTarget : mipmapObject->GetUploadTargets()) { - for (SizeT level = 0; level < probe.mipmapLevels; ++level) { - if (mipmapObject->IsStorageDirty(uploadTarget, level)) { - anyDirty = true; - break; - } - } - if (anyDirty) break; - } - if (!anyDirty) { - MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.", - m_backendTextureId); - return; - } + MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.", + m_backendTextureId); + return; } } @@ -2199,24 +2256,75 @@ namespace MobileGL::MG_Backend::DirectGLES { uploadData, byteSize, &glType, packedUploadData); const IntVec3 uploadSize = GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize); + // Sub-rect upload: when only a region of the level changed (a + // 16x16 sprite in a 1024x512 atlas, the per-frame lightmap) and + // the shadow bytes go to the driver unconverted, upload just that + // region with UNPACK_ROW_LENGTH striding into the level shadow. + // Conversion fallbacks rewrite the whole level into a fresh + // buffer, so they stay on the full-level path, as do targets + // whose backend upload size differs from the shadow's texel size. + const auto dirtyRegion = textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level); + const SizeT texelCount = static_cast(texelSize.x()) * + static_cast(texelSize.y()) * + static_cast(std::max(texelSize.z(), 1)); + const Bool subRectEligible = + uploadData == mipData && !dirtyRegion.Empty() && + !dirtyRegion.CoversWholeLevel(texelSize) && texelCount > 0 && + byteSize % texelCount == 0 && uploadSize.x() == texelSize.x() && + uploadSize.y() == texelSize.y() && + std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1); + const SizeT bpp = subRectEligible ? byteSize / texelCount : 0; + const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(), + dirtyRegion.hi.y() - dirtyRegion.lo.y(), + dirtyRegion.hi.z() - dirtyRegion.lo.z()}; + const SizeT levelRowBytes = static_cast(texelSize.x()) * bpp; + const SizeT levelSliceBytes = static_cast(texelSize.y()) * levelRowBytes; + const Uint8* regionPtr = + static_cast(uploadData) + + static_cast(dirtyRegion.lo.z()) * levelSliceBytes + + static_cast(dirtyRegion.lo.y()) * levelRowBytes + + static_cast(dirtyRegion.lo.x()) * bpp; switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: - g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), glFormat, glType, - uploadData); + if (subRectEligible) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glTexSubImage2D( + glUploadTarget, static_cast(level), dirtyRegion.lo.x(), + dirtyRegion.lo.y(), static_cast(regionSize.x()), + static_cast(regionSize.y()), glFormat, glType, regionPtr); + // The surrounding ScopedDefaultUnpackState shadow says 0. + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } else { + g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast(level), 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), glFormat, + glType, uploadData); + } break; case TextureTarget::Texture3D: case TextureTarget::Texture2DArray: // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly // like a 2D array whose depth is 6 * the cube count. case TextureTarget::TextureCubeMapArray: - g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, - static_cast(uploadSize.x()), - static_cast(uploadSize.y()), - static_cast(uploadSize.z()), glFormat, glType, - uploadData); + if (subRectEligible) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y()); + g_GLESFuncs.glTexSubImage3D( + glUploadTarget, static_cast(level), dirtyRegion.lo.x(), + dirtyRegion.lo.y(), dirtyRegion.lo.z(), + static_cast(regionSize.x()), + static_cast(regionSize.y()), + static_cast(regionSize.z()), glFormat, glType, regionPtr); + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + } else { + g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast(level), 0, 0, 0, + static_cast(uploadSize.x()), + static_cast(uploadSize.y()), + static_cast(uploadSize.z()), glFormat, + glType, uploadData); + } break; default: MGLOG_E("Unhandled texture target %s", @@ -2300,6 +2408,10 @@ namespace MobileGL::MG_Backend::DirectGLES { }); m_prevTextureInfo = currentTextureInfo; + // Everything dirty at entry is uploaded (or provably has no bytes to + // upload); stamp the version so per-draw re-syncs short-circuit until + // the next CPU-side mutation. + m_syncedContentVersion = stateTextureObject->GetContentVersion(); } void BackendTextureObject::SyncBuiltinSamplerToBackend( diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 13adfa98..c6031917 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -266,6 +266,15 @@ namespace MobileGL::MG_Backend::DirectGLES { extern StateBackendObjectRegistry g_backendVertexArrayObjects; + + // Shadowed glBindVertexArray: every backend VAO bind goes through here so a + // draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's + // re-bind) reaches the driver once. Invalidate whenever the ES context is + // replaced - ids restart and the resting binding is 0 again. + void BindBackendVAOId(Uint id); + void InvalidateVAOBindingCache(); + // ES resets the binding to 0 when the currently bound VAO is deleted. + void NoteVAOIdDeleted(Uint id); } // namespace VertexArrayImpl namespace TextureImpl { @@ -375,6 +384,10 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool m_imageBindableStorageRequired = false; Bool m_backendStorageImmutable = false; StateTextureBasicInfo m_prevTextureInfo; + // Frontend content version at the last completed mipmap sync. The per-draw + // clean probe compares this before rebuilding shape info and scanning + // per-level dirty flags; 0 never matches a real version (they start at 1). + Uint64 m_syncedContentVersion = 0; SamplerParameters m_cacheSamplerParameters; UintVec2 m_cacheLodRange = {0, 1000}; FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 5d830302..8f68dead 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -202,9 +202,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) { cacheEntryPair.second.cursor = 0; } - // The frame's descriptor sets are recycled above, so last frame's reuse target - // is gone: start the per-draw descriptor-reuse cache fresh this frame. - m_hasLastDescriptor = false; + // The frame's descriptor sets are recycled above, so last frame's reuse targets + // are gone: start the per-draw descriptor-reuse cache fresh this frame. + for (auto& entry : m_descriptorReuseMemo) { + entry.valid = false; + } m_lastBindValid = false; // Re-fingerprint the bound sampler set fresh this frame so any GL object address // reuse cannot outlive a single frame (see SamplerResolveMemo). @@ -241,8 +243,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } if (purgedSets > 0) { // The per-draw reuse memo folds the layout handle into its signature; drop - // it so a recycled handle value cannot revive a purged set mid-frame. - m_hasLastDescriptor = false; + // every entry so a recycled handle value cannot revive a purged set mid-frame. + for (auto& entry : m_descriptorReuseMemo) { + entry.valid = false; + } MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets); } } @@ -1358,13 +1362,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - // Reuse the previous draw's descriptor set when the resolved content is + // Reuse a recent draw's descriptor set when the resolved content is // byte-identical (only the bind-time dynamic offsets differ). The signature // covers the descriptor-set layout + every write's binding/type/count + the // pointed-to buffer/image/texel-buffer infos (all value-initialized, so no // padding noise). Correctness: bindings are re-resolved every draw, so the // signature always reflects the current state and reuse happens only on an - // exact match; the reused set is never re-acquired within a frame (the acquire + // exact match; a reused set is never re-acquired within a frame (the acquire // cursor only advances), so its written contents survive; the layout is part of // the signature so reuse never crosses programs. Sampler overrides (blits) // bypass and invalidate the cache. @@ -1396,8 +1400,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView)); } - if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) { - descriptorSet = m_lastBoundDescriptorSet; + VkDescriptorSet reusedSet = VK_NULL_HANDLE; + if (cacheable) { + for (const auto& entry : m_descriptorReuseMemo) { + if (entry.valid && entry.signature == signature) { + reusedSet = entry.set; + break; + } + } + } + if (reusedSet != VK_NULL_HANDLE) { + descriptorSet = reusedSet; } else { VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet); if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) { @@ -1411,9 +1424,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (!writes.empty()) { vkUpdateDescriptorSets(m_device, static_cast(writes.size()), writes.data(), 0, nullptr); } - m_lastBoundDescriptorSet = descriptorSet; - m_lastDescriptorSignature = signature; - m_hasLastDescriptor = cacheable; + if (cacheable) { + m_descriptorReuseMemo[m_descriptorReuseMemoNext] = + DescriptorReuseEntry{signature, descriptorSet, true}; + m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize; + } else { + for (auto& entry : m_descriptorReuseMemo) { + entry.valid = false; + } + } } // Skip the driver call when this exact binding is already live on the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 706c75be..e049ab56 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -179,14 +179,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_texelBufferViewsScratch; Vector m_dynamicOffsetsScratch; - // Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers). - // When a draw's resolved descriptor content is byte-identical to the previous - // draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet + - // vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each - // frame in BeginFrame because the frame's descriptor sets are recycled there. - VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE; - Uint64 m_lastDescriptorSignature = 0; - Bool m_hasLastDescriptor = false; + // Descriptor-set reuse across recent draws (see BindProgramUniformBuffers). + // When a draw's resolved descriptor content is byte-identical to one memoized + // earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet + + // vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four + // entries with round-robin replacement rather than one: draws alternating + // between two programs (MC's chunk<->entity ping-pong) would thrash a single + // slot into a full re-allocate+write every draw. Reset each frame in BeginFrame + // because the frame's descriptor sets are recycled there. + struct DescriptorReuseEntry { + Uint64 signature = 0; + VkDescriptorSet set = VK_NULL_HANDLE; + Bool valid = false; + }; + static constexpr Uint32 kDescriptorReuseMemoSize = 4; + DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize]; + Uint32 m_descriptorReuseMemoNext = 0; // vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform // block resolve to the same set AND the same dynamic offsets, so the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index ccbe37a1..f3780ede 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -591,6 +591,34 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + // Idle-content promotion: see the field comments in VkBufferResource. The + // streak counts frame BOUNDARIES survived unchanged (the same-frame memo + // above swallows repeat draws), so a promotion needs the content stable + // for kStreamedPromotionStreak whole frames - one no-op frame does not + // trigger the resident round-trip, whose creation upload is itself a + // staged copy worth avoiding for content that is about to change again. + constexpr Uint32 kStreamedPromotionStreak = 2; + if (resource->promotedResident) { + if (resource->promotedChangeSerial == changeSerial && + static_cast(bufferObject->GetSize()) == size) { + return AcquireResidentSlice(kind, bufferObject, outSlice); + } + resource->promotedResident = false; + resource->unchangedStreak = 0; + } else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size && + resource->transientFrameSerial != 0) { + if (++resource->unchangedStreak >= kStreamedPromotionStreak) { + resource->promotedResident = true; + resource->promotedChangeSerial = changeSerial; + if (AcquireResidentSlice(kind, bufferObject, outSlice)) { + return true; + } + resource->promotedResident = false; // resident creation failed: stream as before + } + } else { + resource->unchangedStreak = 0; + } + if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16, outSlice)) { return false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 25da3f6f..951e7fce 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -62,6 +62,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 transientFrameSerial = 0; Uint64 transientChangeSerial = 0; VkDeviceSize transientSize = 0; + + // Streaming re-copies the whole store into the per-frame arena on every + // frame, which is right for genuinely per-frame data but pure waste for a + // Dynamic-hinted buffer the app stopped touching. After the content + // survives kStreamedPromotionStreak frame boundaries unchanged it is + // promoted to resident storage (one final upload, then zero per-frame + // cost); the first content change demotes it back to streaming, and the + // streaming path's existing downgrade releases the resident store. + Uint32 unchangedStreak = 0; + Bool promotedResident = false; + Uint64 promotedChangeSerial = 0; }; // Supplies a command buffer that is recording and outside any render pass, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 684f13ba..fe4579b4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -25,9 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule // only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the // depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the - // compute stage in addition to the graphics stages. - static constexpr VkPipelineStageFlags kSampledReadStages = - VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; + // compute stage in addition to the graphics stages. Set at Initialize from the renderer's + // device-feature-derived mask: geometry/tessellation stage bits are invalid in a barrier when + // their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS + // would also serialize against non-shader stages. The default only matters before a device + // exists, when nothing records barriers. + static VkPipelineStageFlags s_sampledReadStages = + VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { Int maxDimension = std::max(baseTexelSize.x(), @@ -193,7 +198,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - outSrcStageMask = kSampledReadStages; + outSrcStageMask = s_sampledReadStages; outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT; return; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: @@ -241,7 +246,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: - outDstStageMask = kSampledReadStages; + outDstStageMask = s_sampledReadStages; outDstAccessMask = VK_ACCESS_SHADER_READ_BIT; return; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: @@ -599,6 +604,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_commandPool = initInfo.commandPool; m_graphicsQueue = initInfo.graphicsQueue; m_imageFormatListSupported = initInfo.imageFormatListSupported; + s_sampledReadStages = initInfo.sampledReadStageMask; m_currentFrameIndex = 0; m_deferredReleases.clear(); m_deferredReleases.resize(initInfo.frameCount); @@ -1226,7 +1232,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask, - kSampledReadStages, srcAccessMask, + s_sampledReadStages, srcAccessMask, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels, resource->arrayLayers); MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex()); @@ -2055,6 +2061,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { const void* source = nullptr; Vector expandedData; VkDeviceSize offset = 0; + // Sub-region upload (a small sprite in a big atlas): only the dirty box + // is staged and copied. texelSize keeps the LEVEL extent - the staging + // row copy needs it for the shadow's stride. Plain color formats only; + // the RGB-expand and depth(+stencil) conversion passes rewrite whole + // levels and stay full-size. + Bool subRegion = false; + IntVec3 regionLo = {0, 0, 0}; + IntVec3 regionSize = {0, 0, 0}; + SizeT texelBytes = 0; }; Vector uploadItems; @@ -2101,6 +2116,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { uploadItem.source = source; uploadItem.offset = stagingSize; uploadItem.uploadByteSize = byteSize; + if (!formatInfo.expandRgbToRgba && + GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) { + const auto region = mipmapTexture.GetStorageDirtyRegion(target, level); + const SizeT texelCount = static_cast(texelSize.x()) * + static_cast(texelSize.y()) * + static_cast(std::max(texelSize.z(), 1)); + if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 && + byteSize % texelCount == 0) { + uploadItem.subRegion = true; + uploadItem.regionLo = region.lo; + uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(), + region.hi.z() - region.lo.z()}; + uploadItem.texelBytes = byteSize / texelCount; + uploadItem.uploadByteSize = static_cast(uploadItem.regionSize.x()) * + static_cast(uploadItem.regionSize.y()) * + static_cast(uploadItem.regionSize.z()) * + uploadItem.texelBytes; + } + } if (formatInfo.expandRgbToRgba) { const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo, uploadItem.expandedData); @@ -2255,7 +2289,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { void* mapped = nullptr; VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)"); for (const auto& item : uploadItems) { - std::memcpy(static_cast(mapped) + item.offset, item.source, item.uploadByteSize); + Uint8* dst = static_cast(mapped) + item.offset; + if (!item.subRegion) { + std::memcpy(dst, item.source, item.uploadByteSize); + continue; + } + // Tight-pack the dirty box: the shadow keeps whole-level rows, the + // staging slice holds only the region (bufferRowLength stays 0). + const SizeT levelRowBytes = static_cast(item.texelSize.x()) * item.texelBytes; + const SizeT levelSliceBytes = static_cast(item.texelSize.y()) * levelRowBytes; + const SizeT regionRowBytes = static_cast(item.regionSize.x()) * item.texelBytes; + const Uint8* src = static_cast(item.source); + for (Int z = 0; z < item.regionSize.z(); ++z) { + for (Int y = 0; y < item.regionSize.y(); ++y) { + const Uint8* srcRow = src + + static_cast(item.regionLo.z() + z) * levelSliceBytes + + static_cast(item.regionLo.y() + y) * levelRowBytes + + static_cast(item.regionLo.x()) * item.texelBytes; + std::memcpy(dst + (static_cast(z) * item.regionSize.y() + y) * regionRowBytes, + srcRow, regionRowBytes); + } + } } vmaUnmapMemory(m_allocator, stagingAllocation); @@ -2306,6 +2360,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { copy.imageOffset = {0, 0, 0}; copy.imageExtent = {static_cast(item.texelSize.x()), static_cast(item.texelSize.y()), depthSelectsArrayLayer ? 1u : depthOrLayers}; + if (item.subRegion) { + const Uint32 regionDepth = static_cast(std::max(item.regionSize.z(), 1)); + copy.imageOffset = {item.regionLo.x(), item.regionLo.y(), + depthSelectsArrayLayer ? 0 : item.regionLo.z()}; + copy.imageExtent = {static_cast(item.regionSize.x()), + static_cast(item.regionSize.y()), + depthSelectsArrayLayer ? 1u : regionDepth}; + if (depthSelectsArrayLayer) { + // The GL "depth" axis addresses array layers here, so a partial + // z-range narrows the layer span rather than the extent. + copy.imageSubresource.baseArrayLayer = + item.baseArrayLayer + static_cast(item.regionLo.z()); + copy.imageSubresource.layerCount = regionDepth; + } + } if (isCombinedDepthStencil) { const SizeT texelCount = static_cast(item.texelSize.x()) * static_cast(item.texelSize.y()) * @@ -2330,7 +2399,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { uploadLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, - kSampledReadStages, + s_sampledReadStages, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0, outResource.mipLevels, outResource.arrayLayers); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 8a9b5116..0f0a2c03 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -59,6 +59,12 @@ public: // VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of // formats they will be viewed as, which is what lets a tiler keep them compressed. Bool imageFormatListSupported = false; + // Union of shader stages sampled-read barriers may name on this device; the renderer + // builds it from the enabled features because geometry/tessellation stage bits are + // invalid in a barrier when their feature is off. + VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; }; struct TextureResource { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index dbb561c3..e6664efd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2680,7 +2680,8 @@ void main() { MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); succeeded = m_textureManager->Initialize( {m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue, - m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled}); + m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled, + m_sampledReadStageMask}); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed."); m_clearManager = MakeUnique(); MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); @@ -2935,6 +2936,8 @@ void main() { dlclose(m_platformLibrary); m_platformLibrary = nullptr; } +#endif + if (m_debugMessenger != VK_NULL_HANDLE) { DestroyDebugMessenger(); m_debugMessenger = VK_NULL_HANDLE; @@ -10163,6 +10166,19 @@ void main() { : supportedDeviceFeatures.robustBufferAccess; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader; + // Sampled-read barriers may only name the shader stages whose device feature is + // actually enabled (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), so the + // mask is assembled here, next to the feature decision, and handed to consumers. + m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; + if (deviceFeatures.geometryShader == VK_TRUE) { + m_sampledReadStageMask |= VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT; + } + if (deviceFeatures.tessellationShader == VK_TRUE) { + m_sampledReadStageMask |= VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT | + VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT; + } deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 24af2fe7..03edfa24 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -491,6 +491,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. Bool m_dualSrcBlendFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false; + // Union of shader stages sampled-read barriers may name; built at device creation + // because geometry/tessellation stage bits are invalid in a barrier when their + // feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and + // ALL_GRAPHICS would also serialize against non-shader stages. + VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; // Cached at device creation from the graphics queue family properties // and device limits; drives timer-query support. Uint32 m_timestampValidBits = 0; diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 03a3ed85..26fb242e 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -1073,6 +1073,14 @@ namespace MobileGL::MG_Impl::GLImpl { } MGLOG_D("%s: program = %d, location = %d, byteOffset = %d", __func__, programObject.GetExternalIndex(), location, offset + byteOffsetInsideUniform); + // Apps re-set identical uniform values constantly (Minecraft re-uploads the same + // matrices and sampler indices every frame), and any content-version move makes both + // backends re-upload the whole UBO on the next draw. Every glUniform entry point + // funnels its final bytes through here - after any transpose/stride conversion, with + // the exact destination range known - and the scratch is zero-filled at link (matching + // the GL zero defaults), so a bytes-equal write can be dropped without moving the + // version. + if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return; Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize); programObject.MarkUBOContentDirty(); } else { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index dc8be44e..aea8e676 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -1406,7 +1406,8 @@ namespace MobileGL::MG_Impl::GLImpl { } free(processedPixels); - textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); + textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, zoffset}, + {width, height, depth}); MaybeAutoGenerateMipmap(target, textureObject, false, level); } @@ -1525,7 +1526,8 @@ namespace MobileGL::MG_Impl::GLImpl { free(processedPixels); MGLOG_D("%s: mark mip %d as dirty", __func__, level); - textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); + textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, 0}, + {width, height, 1}); MaybeAutoGenerateMipmap(target, textureObject, false, level); } @@ -1591,7 +1593,7 @@ namespace MobileGL::MG_Impl::GLImpl { } free(processedPixels); - textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true); + textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, 0, 0}, {width, 1, 1}); MaybeAutoGenerateMipmap(target, textureObject, false, level); } @@ -3536,8 +3538,8 @@ namespace MobileGL::MG_Impl::GLImpl { if (texture == 0) { auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); - bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget)); - MG_State::pGLContext->NoteTextureUnitTouched(activeUnit); + const Bool changed = bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget)); + MG_State::pGLContext->NoteTextureUnitTouched(activeUnit, changed); return; } @@ -3571,8 +3573,8 @@ namespace MobileGL::MG_Impl::GLImpl { // ======================= Processing ================================ auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); - bindingSlot.Bind(textureObject); - MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit()); + const Bool changed = bindingSlot.Bind(textureObject); + MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit(), changed); } void ActiveTexture_State(GLenum texture) { @@ -4410,13 +4412,14 @@ namespace MobileGL::MG_Impl::GLImpl { } auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast(unit)); - MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit)); if (texture == 0) { // GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each // target's default texture object (the unit's initial state). + Bool changed = false; for (auto& slot : textureUnit.GetAllBindingSlots()) { - slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget())); + if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true; } + MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit), changed); return; } @@ -4427,7 +4430,8 @@ namespace MobileGL::MG_Impl::GLImpl { MakeUnique("MG_Impl/GLImpl", __func__, "Texture object does not exist.")); return; } - textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject); + const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject); + MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit), changed); } void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index 8479a615..07ed2d2f 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -353,7 +353,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } - Bool ValidateTextureObject(SharedPtr textureObject) { + Bool ValidateTextureObject(const SharedPtr& textureObject) { if (!textureObject) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -376,7 +376,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } - Bool ValidateTextureTargetUniformity(SharedPtr textureObject, + Bool ValidateTextureTargetUniformity(const SharedPtr& textureObject, TextureTarget target) { if (!textureObject) return true; // should be created later TextureTarget prevTarget = textureObject->GetTarget(); @@ -390,7 +390,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } - Bool ValidateTextureSubImageOffsets(SharedPtr textureObject, Int xoffset, + Bool ValidateTextureSubImageOffsets(const SharedPtr& textureObject, Int xoffset, Int width, Int yoffset, Int height, Int zoffset, Int depth) { auto baseSize = textureObject->GetBaseSize(); if (xoffset < 0 || (xoffset + width) > baseSize.x()) { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 355a195e..b5d40b8c 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -30,15 +30,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { TextureInternalFormat internalFormat, TexturePixelDataType type); Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level); - Bool ValidateTextureObject(SharedPtr textureObject); + Bool ValidateTextureObject(const SharedPtr& textureObject); // Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry // points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION // error is generated if zero is bound to target", ARB_texture_storage). Bool ValidateTextureNotDefault(const SharedPtr& textureObject, const char* caller); - Bool ValidateTextureTargetUniformity(SharedPtr textureObject, + Bool ValidateTextureTargetUniformity(const SharedPtr& textureObject, TextureTarget target); - Bool ValidateTextureSubImageOffsets(SharedPtr textureObject, Int xoffset, + Bool ValidateTextureSubImageOffsets(const SharedPtr& textureObject, Int xoffset, Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0); Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2); } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index c4ebb279..21bc78eb 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -41,6 +41,7 @@ namespace MobileGL::MG_State::GLState { void BufferObject::NotifySubData(SizeT offset, SizeT size) { ++m_changeSerial; if (size == 0) return; + m_hasDefinedContent = true; if (g_bufferBackendOps && g_bufferBackendOps->SubData) { g_bufferBackendOps->SubData(*this, offset, size); } @@ -49,12 +50,14 @@ namespace MobileGL::MG_State::GLState { void BufferObject::NotifyFlushMappedRange(Range1D range, Flags appAccess) { ++m_changeSerial; if (range.start >= range.end) return; + m_hasDefinedContent = true; if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) { g_bufferBackendOps->FlushMappedRange(*this, range, appAccess); } } void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) { + m_hasDefinedContent = true; if (m_resource.IsGpuResident()) { // The write already landed in coherent GPU memory; the backend has no separate // copy to sync. Only bump the serial so cached transient slices invalidate. @@ -71,6 +74,9 @@ namespace MobileGL::MG_State::GLState { if (data && size > 0) { Memcpy(m_resource.Bytes(), data, size); } + // A NULL-data respecify (the orphaning idiom) leaves the store undefined; + // record that so backends skip uploading the stale shadow bytes. + m_hasDefinedContent = (data != nullptr) || size == 0; m_isImmutableStorage = false; m_storageFlags = 0; NotifyRespecify(); @@ -89,6 +95,7 @@ namespace MobileGL::MG_State::GLState { } else if (size > 0) { Memset(m_resource.Bytes(), 0, size); } + m_hasDefinedContent = true; m_isImmutableStorage = true; m_storageFlags = storageFlags; NotifyRespecify(); @@ -175,6 +182,7 @@ namespace MobileGL::MG_State::GLState { } void BufferObject::MarkGpuWritten() { + m_hasDefinedContent = true; m_gpuWritePending = true; } @@ -332,6 +340,10 @@ namespace MobileGL::MG_State::GLState { return m_changeSerial; } + Bool BufferObject::HasDefinedContent() const { + return m_hasDefinedContent; + } + const SharedPtr& BufferObject::GetBackendResource() const { return m_resource.Backend(); } diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index c689614e..af47922e 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -188,6 +188,11 @@ namespace MobileGL { // Monotonic counter bumped on every shadow mutation; backends use it to // validate cached transient slices. Uint64 GetChangeSerial() const; + // False after a NULL-data (re)specification until the first content + // write: the app's orphaning idiom (glBufferData with nullptr) leaves + // the store undefined, so backends may (re)allocate GPU storage without + // uploading the stale CPU shadow. + Bool HasDefinedContent() const; const SharedPtr& GetBackendResource() const; void SetBackendResource(SharedPtr resource); @@ -213,6 +218,8 @@ namespace MobileGL { Bool m_isImmutableStorage = false; GLbitfield m_storageFlags = 0; Uint64 m_changeSerial = 0; + // See HasDefinedContent(). + Bool m_hasDefinedContent = true; // Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed. Bool m_gpuWritePending = false; Range1D m_mappedRange; diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index b462a992..d0baf621 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -111,7 +111,9 @@ namespace MobileGL { TextureUnit& GetTextureUnitObject(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit); const ImageTextureBinding& GetImageTextureBinding(Int unit) const; - void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); } + void NoteTextureUnitTouched(Int unit, Bool bindingChanged = true) { + m_textureState.NoteUnitTouched(unit, bindingChanged); + } Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); } // Monotonic counter bumped whenever a texture bind/unbind/delete changes which // texture is bound at a unit; lets a backend skip re-resolving an unchanged diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 51005659..1e356550 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -27,11 +27,23 @@ namespace MobileGL { m_texelSizes.reserve(std::bit_ceil(requiredLevelCount)); m_texelSizes.resize(requiredLevelCount); m_isDirty.resize(requiredLevelCount, false); + m_dirtyRegions.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount); m_compressedFormats.resize(requiredLevelCount, GL_NONE); } m_texelSizes[level] = input.texelSize; + // A respecified level invalidates any pending sub-region: its extents were + // measured against the old size. If the level is still flagged dirty the + // pending upload widens to the whole (new) level. + if (level < m_dirtyRegions.size()) { + m_dirtyRegions[level] = + m_isDirty[level] + ? MipmapDirtyRegion{IntVec3{0, 0, 0}, + IntVec3{input.texelSize.x(), input.texelSize.y(), + std::max(input.texelSize.z(), 1)}} + : MipmapDirtyRegion{}; + } auto& data = m_data[level]; data.resize(input.byteSize, 0); @@ -81,6 +93,7 @@ namespace MobileGL { m_data.resize(levelCount); m_texelSizes.resize(levelCount); m_isDirty.resize(levelCount); + m_dirtyRegions.resize(levelCount); m_compressedData.resize(levelCount); m_compressedFormats.resize(levelCount); } @@ -95,7 +108,7 @@ namespace MobileGL { const Uint8* src = static_cast(input.data); // Clamp so a size mismatch can never write past the allocation. Memcpy(levelData.data(), src, std::min(input.size, levelData.size())); - m_isDirty[level] = true; + MarkDirty(level, true); // whole-level write: dirty region covers everything } } @@ -120,12 +133,51 @@ namespace MobileGL { void MipmapStorage::MarkDirty(Uint level, bool dirty) { MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range"); m_isDirty[level] = dirty; + if (level < m_dirtyRegions.size()) { + if (dirty) { + const IntVec3 size = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0}; + m_dirtyRegions[level] = {IntVec3{0, 0, 0}, + IntVec3{size.x(), size.y(), std::max(size.z(), 1)}}; + } else { + m_dirtyRegions[level] = {}; + } + } } bool MipmapStorage::IsDirty(Uint level) const { MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range"); return m_isDirty[level]; } + + void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) { + MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range"); + const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0}; + MipmapDirtyRegion incoming; + incoming.lo = {std::max(offset.x(), 0), std::max(offset.y(), 0), std::max(offset.z(), 0)}; + incoming.hi = {std::min(offset.x() + size.x(), levelSize.x()), + std::min(offset.y() + size.y(), levelSize.y()), + std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))}; + if (incoming.Empty()) return; + if (level < m_dirtyRegions.size()) { + MipmapDirtyRegion& region = m_dirtyRegions[level]; + if (m_isDirty[level] && !region.Empty()) { + region.lo = {std::min(region.lo.x(), incoming.lo.x()), + std::min(region.lo.y(), incoming.lo.y()), + std::min(region.lo.z(), incoming.lo.z())}; + region.hi = {std::max(region.hi.x(), incoming.hi.x()), + std::max(region.hi.y(), incoming.hi.y()), + std::max(region.hi.z(), incoming.hi.z())}; + } else { + region = incoming; + } + } + m_isDirty[level] = true; + } + + MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const { + if (level >= m_dirtyRegions.size()) return {}; + return m_dirtyRegions[level]; + } } // namespace GLState } // namespace MG_State } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index 2f7761d1..f40454bc 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -7,6 +7,8 @@ // End of Source File Header #pragma once +#include + #include "TextureEnum.h" #include "MG_Util/Types.h" #include "MG_Util/Math/VectorTypes.h" @@ -15,6 +17,22 @@ namespace MobileGL { namespace MG_State { namespace GLState { + // Texel-space bounding box of the shadow bytes a backend has not uploaded + // yet, [lo, hi) per axis. Cleared (all zero) while the level is clean. A + // box, not a range list: repeated sub-image writes union into one region, + // which stays exact for the per-frame "small sub-rect of a big atlas" + // pattern this exists for, and degrades to the old full-level upload as + // the union grows. + struct MipmapDirtyRegion { + IntVec3 lo{0, 0, 0}; + IntVec3 hi{0, 0, 0}; + Bool Empty() const { return hi.x() <= lo.x() || hi.y() <= lo.y() || hi.z() <= lo.z(); } + Bool CoversWholeLevel(const IntVec3& levelSize) const { + return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() && + hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1); + } + }; + class MipmapStorage { public: SizeT GetLevelCount() const; @@ -29,6 +47,12 @@ namespace MobileGL { SizeT GetByteSize(Uint level) const; void MarkDirty(Uint level, bool dirty); bool IsDirty(Uint level) const; + // Union a sub-image write's box into the level's pending region and set the + // dirty flag. MarkDirty keeps its meaning: true covers the whole level, + // false clears the region along with the flag. + void MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size); + // Meaningful only while IsDirty(level). + MipmapDirtyRegion GetDirtyRegion(Uint level) const; // The bytes an application handed to glCompressedTexImage*, kept verbatim beside the // (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires @@ -49,6 +73,7 @@ namespace MobileGL { Vector m_texelSizes; Vector> m_data; Vector m_isDirty; + Vector m_dirtyRegions; Vector> m_compressedData; Vector m_compressedFormats; }; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 3b0ff28b..316ad0b2 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -74,6 +74,16 @@ namespace MobileGL { return m_storage[targetIndex].IsDirty(level); } + void MarkDirtyRegion(Uint targetIndex, Uint level, IntVec3 offset, IntVec3 size) { + MOBILEGL_ASSERT(targetIndex < TargetCount, "MarkDirtyRegion: target invalid"); + m_storage[targetIndex].MarkDirtyRegion(level, offset, size); + } + + MipmapDirtyRegion GetDirtyRegion(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRegion: target invalid"); + return m_storage[targetIndex].GetDirtyRegion(level); + } + void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data, SizeT size) { MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid"); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 5d1f8c1c..712325b7 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -301,6 +301,18 @@ namespace MobileGL { return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, + IntVec3 offset, IntVec3 size) { + ++m_contentVersion; + m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset, + size); + } + + MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) { m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 15d7c362..b5ae5c29 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -150,6 +150,20 @@ namespace MobileGL::MG_State::GLState { virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0; virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0; virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + // Sub-image variant of MarkStorageDirty(..., true): backends may then upload + // only the accumulated region instead of the whole level. The base fallback + // keeps whole-level semantics for storage classes that do not track regions. + virtual void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset, + IntVec3 size) { + (void)offset; + (void)size; + MarkStorageDirty(uploadTarget, mipmapLevel, true); + } + // Meaningful only while IsStorageDirty(uploadTarget, mipmapLevel). + virtual MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const { + const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel); + return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}}; + } // The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim // next to the texel data rather than instead of it - see MipmapStorage. The texel shadow @@ -218,6 +232,9 @@ namespace MobileGL::MG_State::GLState { void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override; bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset, + IntVec3 size) override; + MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) override; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 4a88af09..738c9531 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -55,6 +55,18 @@ namespace MobileGL { return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObject2DCube::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, + IntVec3 offset, IntVec3 size) { + ++m_contentVersion; + m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset, + size); + } + + MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); + } + void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) { m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index 5f9ef831..3d728c76 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -27,6 +27,10 @@ namespace MobileGL { void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override; bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset, + IntVec3 size) override; + MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const override; void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) override; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index be2b8260..0a7c16b3 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -67,7 +67,7 @@ namespace MobileGL::MG_State::GLState { // High-water mark of texture units ever touched by a texture or sampler bind. // Units above it have provably-empty binding slots, so per-draw backend scans // can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units. - void NoteUnitTouched(Int unit) { + void NoteUnitTouched(Int unit, Bool bindingChanged = true) { if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit; // Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit / // glBindTextures / glBindSampler) routes through here, so bumping the generation here @@ -75,7 +75,10 @@ namespace MobileGL::MG_State::GLState { // which texture is bound at which unit. A backend that has cached the per-draw // sampled-texture set can compare this against a snapshot to skip re-resolving it when // no bind changed (the block atlas + lightmap stay bound across a whole terrain batch). - ++m_textureBindGeneration; + // Re-binding the object a slot already holds changes nothing that the generation + // guards; such callers pass bindingChanged=false so only the high-water mark advances + // and the backend fast path survives the redundant re-binds apps issue every frame. + if (bindingChanged) ++m_textureBindGeneration; } Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; } Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; } diff --git a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.cpp b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.cpp index d3191809..a8139876 100644 --- a/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.cpp +++ b/MobileGL/MG_State/GLState/VertexArrayState/VertexArrayState.cpp @@ -34,7 +34,11 @@ namespace MobileGL::MG_State::GLState { } void VertexArrayState::Bind(Uint index) { - m_boundVertexArray = GetVertexArrayObject(index); + const auto& vertexArray = GetVertexArrayObject(index); + // Re-binding the already-current VAO is a per-batch habit of Blaze3D-style renderers; + // skip the shared_ptr store (two atomic refcount ops) when nothing changes. + if (vertexArray == m_boundVertexArray) return; + m_boundVertexArray = vertexArray; } const SharedPtr& VertexArrayState::CreateVertexArrayObject(Uint index) { diff --git a/MobileGL/MG_Util/Types.h b/MobileGL/MG_Util/Types.h index dfeb0621..d34443f1 100644 --- a/MobileGL/MG_Util/Types.h +++ b/MobileGL/MG_Util/Types.h @@ -156,11 +156,14 @@ namespace MobileGL { BindingSlot() : m_target((TargetEnum)0) {} explicit BindingSlot(TargetEnum target) : m_target(target) {} - void Bind(SharedPtr object) { - if (m_boundObject == object) return; + // Reports whether the binding actually changed, so callers can keep change-driven + // bookkeeping (e.g. the texture bind generation) quiet on redundant re-binds. + Bool Bind(SharedPtr object) { + if (m_boundObject == object) return false; m_boundObject = Move(object); ++m_version; + return true; } SharedPtr const& GetBoundObject() const noexcept { return m_boundObject; } TargetEnum GetTarget() const { return m_target; }