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; }