From de6ab16756f46924612ceb00ecdc0d3581c73691 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 6 Oct 2026 21:39:39 -0400 Subject: [PATCH] [Refactor] (P13): the legacy BufferBackendOps table, Magma's never-populated texture/render-pass/renderbuffer caches and VkClearManager go, and the buffer mock tests drive the resource op table instead (ratchet 36 -> 27). --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 46 -- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 430 +----------- MobileGL/MG_Backend/DirectGLES/Managers.h | 36 - .../DirectVulkan/Renderer/PipelineFactory.cpp | 28 - .../DirectVulkan/Renderer/PipelineFactory.h | 9 - .../DirectVulkan/Renderer/VkBufferManager.cpp | 639 ------------------ .../DirectVulkan/Renderer/VkBufferManager.h | 119 +--- .../DirectVulkan/Renderer/VkClearManager.cpp | 18 - .../DirectVulkan/Renderer/VkClearManager.h | 75 -- .../Renderer/VkRenderPassManager.cpp | 228 +------ .../Renderer/VkRenderPassManager.h | 180 +---- .../Renderer/VkTextureManager.cpp | 176 +---- .../DirectVulkan/Renderer/VkTextureManager.h | 96 +-- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 55 +- .../DirectVulkan/Renderer/VulkanRenderer.h | 8 - .../GLState/BufferState/BufferObject.cpp | 91 +-- .../GLState/BufferState/BufferObject.h | 65 -- .../GLState/BufferState/PipeResource.h | 9 +- MobileGL/MG_Test/Buffer/BufferTest.cpp | 75 +- MobileGL/MG_Test/Buffer/SplitBufferTest.cpp | 56 +- scripts/data/link_ratchet_baseline.txt | 11 +- 21 files changed, 102 insertions(+), 2348 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b1b3ce98..bf9842ba 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -455,52 +455,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return request; } - // ---- P5e (sb, MG_Remote/CONTRACT-P5E.md §5.6): THE INDEXED BINDING POINTS BY RECORD -- - // - // WHICH ARM THIS SERVER TAKES, resolved once. Two conjuncts and both are the phase's - // standard shape (§5.8): a live TRANSPORT - the push build under Transport=monolith - // keeps its frontend arms token for token, which is what the verify comparator needs - // and what makes RUN_AHEAD=0 a pure wait-rule A/B on identical server code - AND the - // family bit, so an operator can put the whole binding-point family back on the - // frontend walk with one cleared bit. - // - // THE BINDING-POINT FAMILY'S DEPENDENCY ROW IS READ FROM MG_Pipe/SubsystemDeps.def - // (P3b/P4b R-5, switched over by wave 2-D package D3), refused here rather than half-run, - // in ResolveVertexInputSubsystemArm's exact shape. The hand-rolled - // `bitSet && !resourcesBitSet` this used to be was the sixth statement of D-K2's rule - - // the .def's own "WHO READS IT" section names this function, and now it does. The reason - // is the table's and is printed from it. - Bool ResolveBufferBindingSubsystemArm() { - const Uint64 mask = MG_Config::Features.PipePush; - const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemBufferBindings) != 0; - if (bitSet && - !MG_Pipe::MGPipeSubsystemDependenciesAreSet(MG_Pipe::kMGPipeSubsystemBufferBindings, mask)) { - const Uint64 required = - MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemBufferBindings); - MGLOG_E("MGPipe: kMGPipeSubsystemBufferBindings (bit 13) is set but " - "MOBILEGL_PIPE_PUSH=0x%llx does not carry every bit " - "MG_Pipe/SubsystemDeps.def says it requires (requires 0x%llx, missing " - "0x%llx): %s - REFUSING bit 13 and running the legacy binding-point walk. " - "Set every bit of the row, or clear bit 13", - static_cast(mask), - static_cast(required), - static_cast(required & ~mask), - MG_Pipe::MGPipeSubsystemDependencyWhy(MG_Pipe::kMGPipeSubsystemBufferBindings)); - return false; - } - MGLOG_D("MGPipe: Espryt binding-point family runs the %s arm", - bitSet ? "record" : "legacy"); - return bitSet; - } - - // FILE-LOCAL AND INLINE-MEMOISED, for EsprytSlotTablesEnabled's reason: this is - // consulted on the per-draw path (twice in SyncNeccessaryBuffers, once per UBO in the - // program rebind), and out of line it would be a call through the PLT per consult. - Bool BindingPointsComeFromRecords() { - static const Bool enabled = ResolveBufferBindingSubsystemArm(); - return enabled; - } - // The record arm of SyncBufferBindingPoints. Same shape, same order, same branches; the // four frontend reads become the applier's window and a handle per entry. // diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 7a73b219..c2fc973a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -771,7 +771,6 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace BufferImpl { namespace { using MG_State::GLState::BackendBufferResource; - using MG_State::GLState::BufferBackendOps; using MG_State::GLState::BufferObject; // GL_ARRAY_BUFFER redundant-bind cache (id 0 = unknown/none). @@ -1076,10 +1075,6 @@ namespace MobileGL::MG_Backend::DirectGLES { ring.frameMarks.clear(); } - GLESBufferResource* ResourceOf(BufferObject& bufferObject) { - return static_cast(bufferObject.GetBackendResource().get()); - } - Bool CanTouchGLNow() { return DirectGLES::IsBackendContextCurrentOnThisThread(); } @@ -1581,300 +1576,6 @@ namespace MobileGL::MG_Backend::DirectGLES { constexpr GLbitfield kMapCoherentBit = 0x0080; constexpr GLbitfield kDynamicStorageBit = 0x0100; - // Zero-copy persistent map: back the buffer with real immutable, - // persistently+coherently mapped GL storage (EXT_buffer_storage) and hand the - // app that mapped pointer (adopted by the frontend PipeResource). Returns - // nullptr when the extension is unavailable or the context is not current, in - // which case the frontend keeps its CPU-shadow model. Idempotent. - void* Ops_AcquirePersistentMap(BufferObject& bufferObject) { - if (!CanTouchGLNow() || !g_GLESFuncs.glBufferStorageEXT || !g_GLESFuncs.glMapBufferRange || - !g_GLESFuncs.glGenBuffers) { - return nullptr; - } - const SizeT size = bufferObject.GetSize(); - if (size == 0) return nullptr; - - auto* resource = static_cast(bufferObject.GetBackendResource().get()); - if (!resource) { - auto created = MakeShared(); - resource = created.get(); - bufferObject.SetBackendResource(std::move(created)); - } - // Before the generation is stamped, not after: everything on the resource - // describes a context that is gone, and the idempotency check below would - // otherwise hand the caller the dead context's mapped pointer. - if (resource->contextGeneration != g_bufferContextGeneration) { - resource->id = 0; - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - resource->immutableStorage = false; - resource->storageInitialized = false; - resource->storageSize = 0; - } - resource->contextGeneration = g_bufferContextGeneration; - - if (resource->persistentMapped && resource->persistentPtr && resource->storageSize == size) { - return resource->persistentPtr; // idempotent - } - - // Need a fresh id: glBufferStorage fails on a buffer that already has - // immutable storage, and any prior mutable store is replaced anyway. - if (resource->id != 0) { - NoteBufferIdDeleted(resource->id); - // Driver VAOs may have this id baked into attribute/element bindings - // keyed on frontend versions this re-mint does not move. - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - resource->id = 0; - resource->immutableStorage = false; - } - g_GLESFuncs.glGenBuffers(1, &resource->id); - if (resource->id == 0) return nullptr; - - // Seed from the shadow (MappedData() is still the shadow: the frontend - // adopts and drops it only after this returns). - BindBufferId(TempBufferTarget, resource->id); - const void* initial = bufferObject.MappedData(); - g_GLESFuncs.glBufferStorageEXT(TempBufferTarget, static_cast(size), initial, - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit | - kDynamicStorageBit); - // Set as soon as the store exists, not once the map succeeds: the failure - // path below leaves this id holding immutable storage, and whoever touches - // it next has to know that glBufferData cannot redefine it. - resource->immutableStorage = true; - void* ptr = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), - GL_MAP_WRITE_BIT | kMapPersistentBit | kMapCoherentBit); - if (!ptr) { - MGLOG_E_ONCE("Ops_AcquirePersistentMap: glMapBufferRange(persistent) failed for buffer %u", - resource->id); - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - return nullptr; - } - resource->persistentPtr = ptr; - resource->persistentMapped = true; - resource->storageSize = size; - resource->storageInitialized = true; - resource->pendingRespecify = false; - { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - } - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - return ptr; - } - - void Ops_Respecify(BufferObject& bufferObject) { - auto* resource = ResourceOf(bufferObject); - if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation - // The frontend hands an adopted mapping back before it redefines the store - // (BufferObject::RedefineStorage), so a resource that still carries the - // persistent state here describes the OLD store - and its storage is - // IMMUTABLE (glBufferStorageEXT), which the glBufferData below cannot - // respecify and which the driver would refuse in silence. Retire the id so - // EnsureBufferResource mints a mutable one, with a full upload from the - // shadow the frontend has just filled. - // - // Keyed on the STORAGE, not on persistentMapped: a glMapBufferRange that - // failed after its glBufferStorageEXT succeeded clears persistentMapped and - // still leaves an immutable store behind, and that one reached glBufferData. - if (resource->immutableStorage) { - resource->persistentMapped = false; - resource->persistentPtr = nullptr; - if (resource->id != 0 && CanTouchGLNow() && - resource->contextGeneration == g_bufferContextGeneration) { - NoteBufferIdDeleted(resource->id); - // Frontend VAO bindings survive respecification; force their - // backend twins to bind the replacement buffer name. - ++g_bufferBackendIdGeneration; - g_GLESFuncs.glDeleteBuffers(1, &resource->id); - resource->id = 0; - resource->immutableStorage = false; - } - // Off the context thread the id cannot be deleted here, and dropping it - // would leak an immutable, persistently mapped store. It stays put, and - // stays flagged, until EnsureBufferResource retires it on the thread - // that owns the context. - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = true; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration) { - resource->pendingRespecify = true; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - if (bufferObject.GetSize() == 0) { - resource->storageInitialized = false; - resource->storageSize = 0; - resource->pendingRespecify = false; - resource->pendingRanges.clear(); - resource->pendingResidentWrites.clear(); - return; - } - RespecifyStorageNow(*resource, bufferObject); - } - - void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) { - auto* resource = ResourceOf(bufferObject); - if (!resource) return; - if (resource->pendingRespecify) return; // full re-upload pending anyway - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration || - !StorageMatches(*resource, bufferObject)) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({offset, offset + size}); - return; - } - // An adopted zero-copy persistent store already HAS the bytes (the - // frontend wrote them through the coherent mapping); a driver upload - // here would be a self-copy that re-synchronizes what coherent mapping - // made free. - if (resource->persistentMapped && resource->persistentPtr) { - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - return; - } - // An immediate glBufferSubData resolves the WAR hazard against frames - // still referencing this store on the CPU on some drivers - Mali parks - // the thread in osup_sync_object_wait until every referencing job - // retires, which serialized Minecraft 26.3's per-frame UBO/chunk-mesh - // update streams into ~1 fps. Queue the range instead (the shadow - // already holds the bytes) and let draw-time sync push the merged - // ranges through the staging ring. - if (MG_Config::Features.EsprytDisableUploadRing) { - UploadRangeNow(*resource, bufferObject, offset, offset + size); - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - return; - } - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add({offset, offset + size}); - } - - // App bytes for an ADOPTED store: queue them untouched-by-the-mapping; the - // draw-time sync (or a readback) lands them GPU-ordered through - // DrainResidentWritesNow. No GL here, so the op is thread-agnostic. - void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) { - auto* resource = ResourceOf(bufferObject); - if (!resource || data.size == 0) return; - const std::lock_guard lock(resource->pendingMutex); - auto& write = resource->pendingResidentWrites.emplace_back(); - write.offset = offset; - const auto* bytes = static_cast(data.data); - write.bytes.assign(bytes, bytes + data.size); - } - - void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range, - Flags appAccess) { - auto* resource = ResourceOf(bufferObject); - if (!resource) return; - if (resource->pendingRespecify) return; - if (!CanTouchGLNow() || resource->id == 0 || - resource->contextGeneration != g_bufferContextGeneration || - !StorageMatches(*resource, bufferObject)) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add(range); - return; - } - - // An adopted zero-copy persistent store already HAS the bytes: the - // frontend shadow IS the coherent mapping the app (or UploadSubData) - // wrote into, so publishing is free. The self-copy that used to run - // here mapped a buffer this backend keeps persistently mapped (an - // INVALID_OPERATION whose fallback was a WAR-stalling - // glBufferSubData). - if (resource->persistentMapped && resource->persistentPtr) { - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - return; - } - - // Same WAR-hazard rule as Ops_SubData: an immediate synchronized upload - // (mapped or glBufferSubData) can park the thread on Mali until the - // frames still referencing this store retire. Queue the range for the - // staged flush at draw-time sync; the negative-control kill switch - // keeps the immediate paths below. - if (!MG_Config::Features.EsprytDisableUploadRing) { - const std::lock_guard lock(resource->pendingMutex); - resource->pendingRanges.Add(range); - return; - } - - // Honour the app's real mapping flags per call: only reach for a - // mapped upload when the app allowed invalidation/unsynchronized - // access, otherwise a plain glBufferSubData carries the exact - // synchronization semantics. - const Bool invalidate = (appAccess & BufferMappingAccessBit::InvalidateRange) || - (appAccess & BufferMappingAccessBit::InvalidateBuffer); - const Bool unsynchronized = static_cast(appAccess & BufferMappingAccessBit::Unsynchronized); - if (PREFER_MAP_BUFFER_RANGE_FOR_BUFFER_SYNC && (invalidate || unsynchronized)) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - BindBufferId(TempBufferTarget, resource->id); - void* mappedData = g_GLESFuncs.glMapBufferRange( - TempBufferTarget, (GLintptr)range.start, (GLsizeiptr)(range.end - range.start), - GL_MAP_WRITE_BIT | (invalidate ? GL_MAP_INVALIDATE_RANGE_BIT : 0) | - (unsynchronized ? GL_MAP_UNSYNCHRONIZED_BIT : 0)); - if (mappedData) { - Memcpy(mappedData, bufferObject.MappedData() + range.start, - range.end - range.start); - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - return; - } - MGLOG_E_ONCE("Failed to map buffer with ID: %u for flush, falling back to glBufferSubData", - resource->id); - } - UploadRangeNow(*resource, bufferObject, range.start, range.end); - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - } - - // A shader wrote this buffer through a storage/atomic-counter binding, so the ES - // driver's copy is ahead of the frontend shadow. Pull the whole thing back so - // MapBuffer/GetBufferSubData/CopyBufferSubData see the real results. - void Ops_ReadbackFromGpu(BufferObject& bufferObject) { - auto* resource = ResourceOf(bufferObject); - if (!resource || resource->id == 0 || !resource->storageInitialized) return; - if (!CanTouchGLNow() || resource->contextGeneration != g_bufferContextGeneration) return; - if (resource->persistentMapped) { - // Queued resident SubData bytes land first (GPU-ordered), then the - // finish makes them - and any shader writes already queued on this - // context - visible through the coherent mapping the reads use. - // There is no backend copy to read back in this case. - DrainResidentWritesNow(*resource, bufferObject); - if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish(); - return; - } - if (!g_GLESFuncs.glMapBufferRange || !g_GLESFuncs.glUnmapBuffer) return; - const SizeT size = std::min(bufferObject.GetSize(), resource->storageSize); - if (size == 0) return; - - // Queued app writes must land in the backend store before it is read - // back, or the writeback below would revert them in the shadow. - // The shared ladder, by ID-11: a push build has no FlushPendingRangesNow (see - // the note at its would-be forwarder), so this arm and the handle arm call the - // one body between them. - FlushPendingRangesFrom(*resource, bufferObject.MappedData(), bufferObject.GetSize()); - - BindBufferId(TempBufferTarget, resource->id); - void* mapped = g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast(size), - GL_MAP_READ_BIT); - if (mapped == nullptr) { - MGLOG_E_ONCE("Ops_ReadbackFromGpu: glMapBufferRange(read) failed for buffer %u", resource->id); - return; - } - bufferObject.WritebackFromBackend({mapped, size}, 0); - g_GLESFuncs.glUnmapBuffer(TempBufferTarget); - // The shadow now matches the backend byte for byte; without this the next - // draw would see a newer change serial and re-upload the readback over it. - resource->syncedChangeSerial = bufferObject.GetChangeSerial(); - } - void Ops_OnDestroy(SharedPtr&& resource) { if (!resource) return; auto* glesResource = static_cast(resource.get()); @@ -1911,44 +1612,6 @@ namespace MobileGL::MG_Backend::DirectGLES { g_hasDeferredBufferReleases.store(true, std::memory_order_release); } - // Epoch-tracking wrappers: every op bumps the buffer-mutation epoch AFTER - // its impl returns (release; see Managers.h for why the order matters), - // covering every mutation branch inside - including the early returns - // that only queued pendingRanges or flagged pendingRespecify. Bumping on - // an op that turned out to be a no-op merely re-runs the probes once. - void Ops_RespecifyTracked(BufferObject& bufferObject) { - Ops_Respecify(bufferObject); - BumpBufferMutationEpoch(); - } - void Ops_SubDataTracked(BufferObject& bufferObject, SizeT offset, SizeT size) { - Ops_SubData(bufferObject, offset, size); - BumpBufferMutationEpoch(); - } - void Ops_ResidentSubDataTracked(BufferObject& bufferObject, SizeT offset, DataPtr data) { - Ops_ResidentSubData(bufferObject, offset, data); - BumpBufferMutationEpoch(); - } - void Ops_FlushMappedRangeTracked(BufferObject& bufferObject, Range1D range, - Flags appAccess) { - Ops_FlushMappedRange(bufferObject, range, appAccess); - BumpBufferMutationEpoch(); - } - void Ops_OnDestroyTracked(SharedPtr&& resource) { - Ops_OnDestroy(std::move(resource)); - BumpBufferMutationEpoch(); - } - void* Ops_AcquirePersistentMapTracked(BufferObject& bufferObject) { - void* result = Ops_AcquirePersistentMap(bufferObject); - // Bump even on decline: the frontend still enters a persistent map the - // per-draw probes must start seeing (IsMapped-driven range pushes). - BumpBufferMutationEpoch(); - return result; - } - void Ops_ReadbackFromGpuTracked(BufferObject& bufferObject) { - Ops_ReadbackFromGpu(bufferObject); - BumpBufferMutationEpoch(); - } - // ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------ // // Every body below is its Ops_* counterpart above with exactly the substitutions @@ -3113,15 +2776,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif }; - const BufferBackendOps g_glesBufferBackendOps = { - .Respecify = Ops_RespecifyTracked, - .SubData = Ops_SubDataTracked, - .ResidentSubData = Ops_ResidentSubDataTracked, - .FlushMappedRange = Ops_FlushMappedRangeTracked, - .OnDestroy = Ops_OnDestroyTracked, - .AcquirePersistentMap = Ops_AcquirePersistentMapTracked, - .ReadbackFromGpu = Ops_ReadbackFromGpuTracked, - }; } // namespace Uint64 CurrentBufferMutationEpoch() { @@ -3250,76 +2904,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return PipeSubsystemArmVerdict::NoArm; } - Bool ResolveResourceSubsystemArm() { - const Bool bitSet = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemResources) != 0; - // The buffer family's legacy arm is the Ops_* table and g_glesBufferBackendOps, which - // are compiled UNCONDITIONALLY - only the VAO twin's memos and the pre-handle VAO - // body live under MOBILEGL_PIPE_LEGACY_MEMOS - so this family cannot be left armless - // and NoArm is unreachable for it. Said out loud rather than assumed, because the - // knob is still answered below. - const PipeSubsystemArmVerdict verdict = - ClassifyPipeSubsystemArm(bitSet, /*legacyMemosEnabled=*/true, - /*legacyArmSurvivesLegacyMemos=*/true); - // Unreachable for this family by the argument above, and stated as a stop anyway: - // the two P3a families answer an armless verdict the same way, and an unreachable - // branch that says something different is how the reachable one drifts. - if (verdict == PipeSubsystemArmVerdict::NoArm) { - StopOnArmlessPipeSubsystem("MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemResources " - "(bit 7) clear and the pre-handle buffer arm is gone"); - } - const Bool enabled = verdict == PipeSubsystemArmVerdict::Handles; - MGLOG_D("MGPipe: Espryt resource family runs the %s arm", enabled ? "handle" : "legacy"); - return enabled; - } - - Bool ResolveVertexInputSubsystemArm() { - const Uint64 mask = MG_Config::Features.PipePush; - const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemVertexInput) != 0; - // The vertex-input family's row, READ FROM MG_Pipe/SubsystemDeps.def (P3b/P4b R-5, - // switched over by wave 2-D package D3) rather than restated as the hand-rolled - // `bitSet && !resourcesBitSet` this function used to carry - one of the six statements - // of D-K2's rule that R-5 found with nothing comparing them. The table's own sentence - // is what the refusal prints. The mirror pair (bit 7 set, bit 8 clear) is fine - the - // legacy VAO walk calls BindAttributeBuffer -> EnsureBufferResource, which dispatches - // to the handle arm by itself - and lives with the other mirror pairs in MGPipe.h. - if (bitSet && !MG_Pipe::MGPipeSubsystemDependenciesAreSet(MG_Pipe::kMGPipeSubsystemVertexInput, mask)) { - MGLOG_E("MGPipe: kMGPipeSubsystemVertexInput (bit 8) is set but MOBILEGL_PIPE_PUSH=" - "0x%llx does not carry every bit MG_Pipe/SubsystemDeps.def says it requires " - "(requires 0x%llx, missing 0x%llx): %s - REFUSING bit 8 and running the " - "legacy vertex-input arm. Set every bit of the row, or clear bit 8", - static_cast(mask), - static_cast( - MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemVertexInput)), - static_cast( - MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemVertexInput) & ~mask), - MG_Pipe::MGPipeSubsystemDependencyWhy(MG_Pipe::kMGPipeSubsystemVertexInput)); - return false; - } - constexpr Bool kLegacyVaoArmCompiled = false; - // Unlike the buffer family, this one's legacy arm IS conditional: the pre-handle - // SyncToBackend body and the twin memos it reads are inside MOBILEGL_PIPE_LEGACY_MEMOS. - const PipeSubsystemArmVerdict verdict = - ClassifyPipeSubsystemArm(bitSet, /*legacyMemosEnabled=*/true, - /*legacyArmSurvivesLegacyMemos=*/false); - if (verdict != PipeSubsystemArmVerdict::Handles && - (verdict == PipeSubsystemArmVerdict::NoArm || !kLegacyVaoArmCompiled)) { - // M-4: A STOP, not a log line. Continuing here left the process drawing through - // an UNCONFIGURED driver VAO - every attribute pointer whatever the last owner - // of that VAO name set it to - after one MGLOG_E that a lane summary does not - // read. The lanes that pin MOBILEGL_PIPE_LEGACY_MEMOS=0 all carry an explicit - // 0x1ff now, so this verdict is an operator's configuration and nothing else. - StopOnArmlessPipeSubsystem( - kLegacyVaoArmCompiled - ? "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemVertexInput (bit 8) clear and " - "MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle VAO sync" - : "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemVertexInput (bit 8) clear and " - "the pre-handle VAO sync is not compiled into this build"); - } - const Bool enabled = verdict == PipeSubsystemArmVerdict::Handles; - MGLOG_D("MGPipe: Espryt vertex-input family runs the %s arm", enabled ? "handle" : "legacy"); - return enabled; - } - GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; // The table refuses both of these itself; this is the release-build VOICE for the @@ -3544,10 +3128,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 g_bufferBackendIdGeneration = 0; void RegisterBufferBackendOps() { - MG_State::GLState::SetBufferBackendOps(&g_glesBufferBackendOps); - // P3a: the handle-shaped table goes up beside it, at the same two bring-up sites - // and with the same lifetime. Registration is UNCONDITIONAL, exactly as - // BufferBackendOps' is: the subsystem bit is the FRONTEND's dispatch predicate + // P3a: the handle-shaped resource table (P13: the only one - the legacy + // BufferBackendOps table is deleted). Registration is UNCONDITIONAL: the subsystem bit is the FRONTEND's dispatch predicate // (MGPipeResourceSubsystemEnabled reads the bit AND this table's presence), so a // build with bit 7 clear registers a table nobody calls and the A/B stays a pure // configuration question rather than a bring-up-order one. @@ -3563,9 +3145,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } void UnregisterBufferBackendOps() { - if (MG_State::GLState::GetBufferBackendOps() == &g_glesBufferBackendOps) { - MG_State::GLState::SetBufferBackendOps(nullptr); - } if (MG_Pipe::MGPipeGetResourceOps() == &g_glesResourceOps) { MG_Pipe::MGPipeSetResourceOps(nullptr); } @@ -3680,11 +3259,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } #endif - GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject) { - if (!bufferObject) return nullptr; - return static_cast(bufferObject->GetBackendResource().get()); - } - // The same five questions, asked of the applier instead of the frontend object, with // IDENTICAL semantics (D-A4): // identity the slot table's twin at this handle, not GetBackendResource() diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 4851a3a0..f98a854f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -540,10 +540,6 @@ namespace MobileGL::MG_Backend::DirectGLES { static const Bool enabled = ResolveFramebufferSubsystemArm(); return enabled; } - inline Bool TextureResourceSubsystemEnabled() { - static const Bool enabled = ResolveTextureResourceSubsystemArm(); - return enabled; - } inline Bool SamplerSubsystemEnabled() { static const Bool enabled = ResolveSamplerSubsystemArm(); return enabled; @@ -885,36 +881,6 @@ namespace MobileGL::MG_Backend::DirectGLES { SlotTableRegistry; extern BackendBufferResourceTable g_backendBufferResources; - // Resolved once per process and latched, exactly like EsprytSlotTablesEnabled() and - // for the same reason: the two arms hold GLESBufferResource in DIFFERENT containers - - // the legacy arm in the frontend object's PipeResource::m_backend, the handle arm in - // the table above - so an answer that changed mid-run would strand every resource - // already built and leak the driver ids they own. - Bool ResolveResourceSubsystemArm(); - // Same shape for the vertex-input family (bit 8), and separate because the two bits are - // separately clearable - but NOT independent, and the resolver says so out loud rather - // than half-running: bit 8 REQUIRES bit 7, because the vertex-input handle arm resolves - // every attribute's driver buffer id out of the resource slot table and only bit 7 puts - // twins there. `0x17f` (bit 8 on, bit 7 off) is therefore refused at arm resolution with - // a named MGLOG_E and runs the legacy vertex-input arm; `0x0ff` (bit 7 on, bit 8 off) is - // a real, supported A/B, because the legacy VAO walk reaches the handle arm through - // EnsureBufferResource's own dispatch. Both resolvers also answer - // MG_Config::Features.PipeLegacyMemos, so "the bit is clear and the legacy arm was taken - // away" is a named verdict instead of a silent legacy run. - Bool ResolveVertexInputSubsystemArm(); - - // INLINE for the reason SlotTables.h spells out at EsprytSlotTablesEnabled: both are - // consulted on the per-draw path (the VAO sync's gate, EnsureBufferResource, every - // buffer op), and out-of-line they would be a call through the PLT per consult. - inline Bool ResourceSubsystemEnabled() { - static const Bool enabled = ResolveResourceSubsystemArm(); - return enabled; - } - inline Bool VertexInputSubsystemEnabled() { - static const Bool enabled = ResolveVertexInputSubsystemArm(); - return enabled; - } - // Resolve-or-create / resolve-only, by the handle the call carried. Neither touches // MGPipeSlots(): the handle ARRIVED already minted by the side that owns minting. GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res); @@ -1059,8 +1025,6 @@ namespace MobileGL::MG_Backend::DirectGLES { void OnServerSessionEndedWithOthersLive(); void OnBackendContextDestroyed(); - // Existing resource or nullptr; performs no GL calls. - GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject); // Deletes GL buffers whose owning frontend objects died (possibly on a // thread without a current ES context). Called from draw-time sync. void ProcessDeferredBufferReleases(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 883147ad..ffeb571a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -337,34 +337,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return evicted; } - Uint32 PipelineFactory::EvictByRenderPasses(const Vector& renderPasses) { - if (renderPasses.empty() || m_cache.empty()) { - return 0; - } - // Sorted-batch membership test keeps a mass eviction (shader-pack switch, - // dimension exit) at one O(cache * log batch) scan instead of one full scan - // per dying pass. - Vector sortedPasses = renderPasses; - std::sort(sortedPasses.begin(), sortedPasses.end()); - Uint32 evicted = 0; - for (auto it = m_cache.begin(); it != m_cache.end();) { - if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) { - if (it->second.pipeline != VK_NULL_HANDLE) { - vkDestroyPipeline(m_device, it->second.pipeline, nullptr); - } - it = m_cache.erase(it); - ++evicted; - } else { - ++it; - } - } - if (evicted > 0) { - MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes", - evicted, sortedPasses.size()); - } - return evicted; - } - Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) { Uint32 evicted = 0; for (auto it = m_cache.begin(); it != m_cache.end();) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 4823b7ee..329640a6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -140,15 +140,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines // destroyed so the caller can drop any memoized VkPipeline handle. Uint32 OnFrameBoundary(); - // Destroys every cached pipeline hashed on one of `renderPasses`. Only safe - // when the caller guarantees GPU idleness for them - the render-pass manager - // calls this (via the renderer) for passes its own >1024-boundary-idle sweep - // just evicted, and a pipeline hashed on those handles is only ever bound by - // draws that also hit the render-pass entries. Also closes the handle-recycling - // hazard: a recycled VkRenderPass value must never serve a stale pipeline. - // Batched: one cache scan regardless of how many passes died in the sweep. - // Returns the number destroyed (callers invalidate memos when non-zero). - Uint32 EvictByRenderPasses(const Vector& renderPasses); // Destroys every cached pipeline built from the program with content hash // `programHash`. Called from the ProgramFactory eviction path, which proves the // same >1024-boundary idleness (the program's pipelines are only bound by draws diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 41d8c720..3779dc0d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -105,78 +105,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - using MG_State::GLState::BackendBufferResource; - using MG_State::GLState::BufferBackendOps; using MG_State::GLState::BufferObject; // The manager owned by the active VulkanRenderer; immediate ops route here. SessionLocal g_activeBufferManager; // per Magma session (MagmaSession.h) - void Ops_Respecify(BufferObject& bufferObject) { - if (g_activeBufferManager.Get()) { - g_activeBufferManager.Get()->OnRespecify(bufferObject); - } - } - - void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) { - if (g_activeBufferManager.Get()) { - g_activeBufferManager.Get()->OnSubData(bufferObject, offset, size); - } - } - - void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) { - if (g_activeBufferManager.Get()) { - g_activeBufferManager.Get()->OnResidentSubData(bufferObject, offset, data); - } - } - - void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range, - Flags appAccess) { - if (g_activeBufferManager.Get()) { - g_activeBufferManager.Get()->OnFlushMappedRange(bufferObject, range, appAccess); - } - } - - // The CPU is about to read a buffer a shader wrote. Its bytes live in coherent - // host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is - // bound as a shader storage buffer), so nothing needs copying - but coherence only - // says the writes are visible once they have happened, so the work has to retire - // first, including copies already submitted by a sync-point flush. - void Ops_ReadbackFromGpu(BufferObject& bufferObject) { - (void)bufferObject; - if (pVulkanRenderer) { - pVulkanRenderer->WaitForSubmitIndex( - pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true); - } - } - - void* Ops_AcquirePersistentMap(BufferObject& bufferObject) { - if (g_activeBufferManager.Get()) { - return g_activeBufferManager.Get()->AcquirePersistentMap(bufferObject); - } - return nullptr; - } - - void Ops_OnDestroy(SharedPtr&& resource) { - if (g_activeBufferManager.Get()) { - g_activeBufferManager.Get()->OnResourceDestroyed(std::move(resource)); - } - // No active manager: the device/allocator is gone or going away and - // Shutdown() already destroyed the storage; dropping the handle here - // must not touch Vulkan. VkBufferResource's dtor destroys via VMA only - // when the allocation is still valid, which Shutdown() cleared. - } - - const BufferBackendOps g_vulkanBufferBackendOps = { - .Respecify = Ops_Respecify, - .SubData = Ops_SubData, - .ResidentSubData = Ops_ResidentSubData, - .FlushMappedRange = Ops_FlushMappedRange, - .OnDestroy = Ops_OnDestroy, - .AcquirePersistentMap = Ops_AcquirePersistentMap, - .ReadbackFromGpu = Ops_ReadbackFromGpu, - }; - // The session's manager, or null once its renderer is gone - a client's records can still // arrive after its eglTerminate released it (or, surfaceless, before its first surface built // one). Such a record is declined with one line rather than taking the whole server down. @@ -294,7 +227,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource->gpuWritesPending = false; resource->indirectReadBarrierPending = false; resource->stagedCoverage.clear(); - ++m_sliceEpochCounter; if (resource->size == 0) return; VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT; @@ -455,7 +387,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { MG_Record::StagedShadowStore::CoverageAdd(resource->stagedCoverage, static_cast(offset), static_cast(offset + size)); - ++m_sliceEpochCounter; } void VkBufferManager::FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, @@ -621,7 +552,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::abort(); } if (t0) { - ++m_sliceEpochCounter; return; } if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) { @@ -632,7 +562,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // this owned vector is released; no mapped Vulkan pointer crosses roles. MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(res, offset, {reinterpret_cast(bytes.data()), size, MG_Pipe::kMGHostSpanSegNone, 0}); - ++m_sliceEpochCounter; } void VkBufferManager::MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) { @@ -644,7 +573,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource->stagedCoverage.clear(); resource->gpuWritesPending = true; resource->indirectReadBarrierPending = true; - ++m_sliceEpochCounter; // The existing client dirty-state protocol accepts one whole-store // notification. The store remains on the GPU and exact later SubData // only overwrites its own range, preserving every other GPU-written byte. @@ -787,7 +715,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource->gpuWritesPending = false; resource->stagedCoverage.clear(); MG_Record::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast(size)); - ++m_sliceEpochCounter; return mapped; } @@ -822,7 +749,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The whole store holds bytes the client supplied (its AHB, seeded from its shadow). resource->stagedCoverage.clear(); MG_Record::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast(size)); - ++m_sliceEpochCounter; ++m_wireStoreCount; NoteWireStorePeaks(); PublishWireReclaimGauges(); @@ -1067,7 +993,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial) : found->second.lastUseSerial); // P11 B2, as Respecify m_wireBuffers.erase(found); - ++m_sliceEpochCounter; PublishWireReclaimGauges(); } @@ -1208,7 +1133,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_initInfo = initInfo; m_deferredBufferReleases.resize(initInfo.frameCount); - m_deferredResourceReleases.resize(initInfo.frameCount); m_currentFrameIndex = 0; m_frameSerial = 1; m_completedSerialFloor = 0; @@ -1216,7 +1140,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } g_activeBufferManager.Get() = this; - MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps); RegisterWireResourceOps(); return true; } @@ -1232,15 +1155,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) { MG_Pipe::MGPipeSetResourceOps(nullptr); } - if (MG_State::GLState::GetBufferBackendOps() == &g_vulkanBufferBackendOps) { - MG_State::GLState::SetBufferBackendOps(nullptr); - } } m_transientUploadArena.Shutdown(); m_unboundStorageBuffer.Destroy(); m_unboundTexelBuffer.Destroy(); DestroyAllDeferredReleases(); - ReleaseAllLiveResources(); // DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the // records still hold, so nothing this arm minted outlives the count. if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch; @@ -1267,7 +1186,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_initInfo.frameCount = frameCount; DestroyAllDeferredReleases(); m_deferredBufferReleases.resize(frameCount); - m_deferredResourceReleases.resize(frameCount); m_currentFrameIndex = 0; return InitializeTransientArenas(); } @@ -1335,10 +1253,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return std::max(completed, m_completedSerialFloor); } - Bool VkBufferManager::IsResourceBusy(const VkBufferResource& resource) const { - return resource.lastUseSerial > GetCompletedSerial(); - } - Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) { if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) { @@ -1390,153 +1304,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { }); } - VkBufferResource* VkBufferManager::ResourceOf(MG_State::GLState::BufferObject& bufferObject) { - return static_cast(bufferObject.GetBackendResource().get()); - } - - VkBufferResource* VkBufferManager::GetOrCreateResource( - const SharedPtr& bufferObject) { - // Return by raw pointer: the resource is owned for its whole lifetime by the BufferObject's - // backend-resource SharedPtr (already set, or set below), so callers that only dereference - // it avoid a static_pointer_cast + SharedPtr refcount inc/dec on every per-draw buffer bind. - const auto& existing = bufferObject->GetBackendResource(); - if (existing) { - return static_cast(existing.get()); - } - auto resource = MakeShared(); - VkBufferResource* raw = resource.get(); - bufferObject->SetBackendResource(resource); - TrackLiveResource(resource); - return raw; - } - - void VkBufferManager::TrackLiveResource(const SharedPtr& resource) { - // Sweep on a doubling watermark rather than on every insert past the threshold. The old - // form walked the whole vector for each new buffer once the list passed 256, and when the - // buffers are all live the walk removes nothing and the list grows by one - so creating N - // live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep - // still removes exactly the expired entries, just less often and with the same bound on how - // much dead weight can accumulate (at most as many entries as were live at the last sweep). - if (m_liveResources.size() >= std::max(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) { - std::erase_if(m_liveResources, [](const WeakPtr& weak) { return weak.expired(); }); - m_liveResourcesLastPruned = m_liveResources.size(); - } - m_liveResources.push_back(resource); - } - - void VkBufferManager::ReleaseAllLiveResources() { - for (auto& weak : m_liveResources) { - if (auto resource = weak.lock()) { - BumpSliceEpoch(*resource); - resource->buffer.Destroy(); - resource->storageSize = 0; - resource->usageFlags = 0; - resource->lastUseSerial = 0; - resource->pendingFullUpload = true; - resource->transientSlice = {}; - resource->transientFrameSerial = 0; - } - } - m_liveResources.clear(); - } - - Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, - VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) { - // The only place a resident VkBuffer handle is minted, so every resident slice - // change funnels through here (callers release the old handle first). - BumpSliceEpoch(resource); - // Staged range copies write resident storage with vkCmdCopyBuffer. - usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT; - const Bool created = resource.buffer.Create({ - .allocator = m_initInfo.allocator, - .size = size, - .usage = usage, - .memoryUsage = VMA_MEMORY_USAGE_AUTO, - .allocationFlags = kResidentBufferAllocationFlags, - .requiredFlags = requiredFlags, - }); - if (!created || resource.buffer.Map() == nullptr) { - MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)", - static_cast(size)); - resource.buffer.Destroy(); - resource.storageSize = 0; - resource.usageFlags = 0; - return false; - } - resource.storageSize = size; - resource.usageFlags = usage; - return true; - } - - Bool VkBufferManager::SwapStorageAndUploadAll(VkBufferResource& resource, - MG_State::GLState::BufferObject& bufferObject) { - const VkDeviceSize size = static_cast(bufferObject.GetSize()); - const VkBufferUsageFlags usage = resource.usageFlags; - DeferRelease(std::move(resource.buffer)); - if (!CreateResidentStorage(resource, size, usage)) { - resource.pendingFullUpload = true; - return false; - } - if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) { - MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed"); - resource.pendingFullUpload = true; - return false; - } - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast(size)); - } - resource.pendingFullUpload = false; - return true; - } - - Bool VkBufferManager::StagedRangeCopy(VkBufferResource& resource, const void* data, - SizeT offset, SizeT size) { - if (!m_copyProvider) { - return false; - } - BufferSlice staging{}; - if (!m_transientUploadArena.Upload(m_currentFrameIndex, data, - static_cast(size), 16, staging)) { - return false; - } - if (MG_Util::PipeStats::Enabled()) { - // The staging fill is the host copy; the vkCmdCopyBuffer below is the device - // half of the same bytes and is not counted twice. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast(size)); - } - VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer(); - if (commandBuffer == VK_NULL_HANDLE) { - return false; - } - - // Order the copy after every prior read/write of this buffer, both from - // in-flight frames (submission order) and from commands already recorded - // in this frame's command buffer. - VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; - beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; - beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - EndActiveRenderPassOn(commandBuffer); - vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, - &beforeBarrier, 0, nullptr, 0, nullptr); - - VkBufferCopy region{}; - region.srcOffset = staging.offset; - region.dstOffset = static_cast(offset); - region.size = static_cast(size); - EndActiveRenderPassOn(commandBuffer); - vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion); - - VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; - afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; - EndActiveRenderPassOn(commandBuffer); - vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, - &afterBarrier, 0, nullptr, 0, nullptr); - - resource.lastUseSerial = m_frameSerial; - return true; - } - Bool VkBufferManager::StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size) { if (!m_copyProvider) { @@ -1590,381 +1357,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } - void VkBufferManager::OnRespecify(MG_State::GLState::BufferObject& bufferObject) { - auto* resource = ResourceOf(bufferObject); - if (!resource) { - return; // lazy: AcquireResidentSlice performs a full upload on creation - } - // A respecify can change the size, the usage hint (so the resident/streamed - // route), and the contents at once; retire every memo before deciding what to - // do about the storage. - BumpSliceEpoch(*resource); - // Any cached streaming slice refers to the previous contents. - resource->transientFrameSerial = 0; - // Redefining the store hands any adopted mapping back to the CPU shadow - // (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped - // is an ordinary resident one again: it needs the busy-tracking and conditional - // orphan below, and the next AcquirePersistentMap has to mint storage for the new - // store rather than hand back a mapping of the old one. - resource->persistentMapped = false; - if (!resource->buffer.IsValid()) { - return; // streaming-only resource: shadow + serial are enough - } - - const VkDeviceSize size = static_cast(bufferObject.GetSize()); - if (size == 0) { - DeferRelease(std::move(resource->buffer)); - resource->storageSize = 0; - resource->pendingFullUpload = false; - return; - } - - if (size != resource->storageSize || IsResourceBusy(*resource)) { - // Conditional orphan: only swap the storage when the old one is - // still referenced by the GPU (or no longer fits). - SwapStorageAndUploadAll(*resource, bufferObject); - return; - } - - if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) { - MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed"); - resource->pendingFullUpload = true; - } else if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast(size)); - } - } - - void VkBufferManager::OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size) { - auto* resource = ResourceOf(bufferObject); - if (!resource) { - return; - } - // Drops the streaming memo below and may end in a storage swap or a deferred - // full re-upload, so no memoised slice survives this. - BumpSliceEpoch(*resource); - resource->transientFrameSerial = 0; - if (!resource->buffer.IsValid() || resource->pendingFullUpload) { - return; - } - if (static_cast(bufferObject.GetSize()) != resource->storageSize) { - resource->pendingFullUpload = true; - return; - } - - if (!IsResourceBusy(*resource)) { - if (!resource->buffer.Upload(bufferObject.MappedData() + offset, - static_cast(size), static_cast(offset))) { - MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed"); - resource->pendingFullUpload = true; - } else if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - return; - } - - // Busy partial write: stage + GPU copy preserves GL ordering within the - // frame and leaves bytes outside the range (possibly GPU-written, e.g. - // SSBO) intact. Fall back to a storage swap if staging is unavailable. - if (!StagedRangeCopy(*resource, bufferObject.MappedData() + offset, offset, size)) { - SwapStorageAndUploadAll(*resource, bufferObject); - } - } - - void VkBufferManager::OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, - SizeT offset, DataPtr data) { - auto* resource = ResourceOf(bufferObject); - MOBILEGL_ASSERT(resource && resource->persistentMapped && resource->buffer.IsValid(), - "OnResidentSubData requires adopted Vulkan storage"); - // The mapping is also the GPU's storage. Copy the supplied bytes onto the - // command timeline before touching it: earlier draws must keep seeing the - // old contents, including draws recorded but not yet submitted. The buffer - // cannot be orphaned because the application may hold its mapped pointer. - if (StagedRangeCopy(*resource, data.data, offset, data.size)) { - return; - } - // Allocation failure: a host write is safe only after all prior work retires. - if (pVulkanRenderer && pVulkanRenderer->WaitForSubmitIndex( - pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) { - resource->buffer.Upload(data.data, data.size, offset); - } else { - MGLOG_E_ONCE("VkBufferManager::OnResidentSubData: ordered upload failed"); - } - } - - void VkBufferManager::OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range, - Flags appAccess) { - auto* resource = ResourceOf(bufferObject); - if (!resource) { - return; - } - BumpSliceEpoch(*resource); - resource->transientFrameSerial = 0; - if (!resource->buffer.IsValid() || resource->pendingFullUpload) { - return; - } - if (static_cast(bufferObject.GetSize()) != resource->storageSize) { - resource->pendingFullUpload = true; - return; - } - - const SizeT offset = range.start; - const SizeT size = range.end - range.start; - // GL_MAP_UNSYNCHRONIZED_BIT: the app guarantees it does not overwrite - // data the GPU is still reading; honour it with a direct host write. - if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) { - if (!resource->buffer.Upload(bufferObject.MappedData() + offset, - static_cast(size), static_cast(offset))) { - MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed"); - resource->pendingFullUpload = true; - } else if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - return; - } - - if (!StagedRangeCopy(*resource, bufferObject.MappedData() + offset, offset, size)) { - SwapStorageAndUploadAll(*resource, bufferObject); - } - } - - void VkBufferManager::OnResourceDestroyed(SharedPtr&& resource) { - if (!resource) { - return; - } - auto vkResource = std::static_pointer_cast(std::move(resource)); - if (!vkResource->buffer.IsValid()) { - return; - } - if (m_deferredResourceReleases.empty()) { - vkResource->buffer.Destroy(); - return; - } - MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredResourceReleases.size(), - "VkBufferManager::OnResourceDestroyed current frame index out of range"); - // Keep the whole resource alive until this frame slot's fence has been - // waited, then the storage is destroyed with it. - m_deferredResourceReleases[m_currentFrameIndex].push_back(std::move(vkResource)); - } - - void* VkBufferManager::AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) { - const VkDeviceSize size = static_cast(bufferObject.GetSize()); - if (size == 0) { - return nullptr; - } - - auto resource = std::static_pointer_cast(bufferObject.GetBackendResource()); - if (!resource) { - resource = MakeShared(); - bufferObject.SetBackendResource(resource); - TrackLiveResource(resource); - } - - // Bumped for the request, not just for the storage it may create. This is the - // one call the frontend makes when a buffer becomes persistently mapped for - // writing (BufferObject::AcquireMemoryRange), and a map the backend declines - // keeps mutating its shadow with no further API call - so it is what lets - // GetSliceEpochCounter stand for "no buffer needs a persistent-map range push". - BumpSliceEpoch(*resource); - - // Idempotent: an already-backed buffer returns the same mapped base. - if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) { - return resource->buffer.GetMappedData(); - } - - // One-time creation of HOST_VISIBLE + HOST_COHERENT, persistently mapped storage - // carrying every usage (never recreated, so the app's pointer never dangles). Seed - // it from the current shadow - MappedData() is still the shadow here because the - // frontend adopts (and drops) the shadow only after this returns. - DeferRelease(std::move(resource->buffer)); - const VkBufferUsageFlags persistentUsage = - kPersistentBackedUsage | - (m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0); - if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) { - resource->persistentMapped = false; - resource->storageSize = 0; - resource->usageFlags = 0; - return nullptr; - } - const Uint8* seed = bufferObject.MappedData(); - if (seed != nullptr) { - resource->buffer.Upload(seed, size, 0); - if (MG_Util::PipeStats::Enabled()) { - // The one-time seed of a persistent map. Everything the app writes AFTER - // this goes straight through the mapping and is persistent-map-push - // territory (unwired, D4/D-B4), not this class. - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - } - resource->persistentMapped = true; - resource->pendingFullUpload = false; - resource->storageSize = size; - resource->lastUseSerial = 0; - return resource->buffer.GetMappedData(); - } - - Bool VkBufferManager::AcquireResidentSlice(BufferKind kind, - const SharedPtr& bufferObject, - BufferSlice& outSlice) { - const VkBufferUsageFlags requiredUsage = GetVkBufferUsage(kind); - MOBILEGL_ASSERT(requiredUsage != 0, "VkBufferManager::AcquireResidentSlice unsupported buffer kind"); - MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireResidentSlice requires valid buffer object"); - - auto resource = GetOrCreateResource(bufferObject); - bufferObject->SyncPersistentMappedRange(); - - const VkDeviceSize size = static_cast(bufferObject->GetSize()); - if (size == 0) { - MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero"); - return false; - } - - // Zero-copy persistent buffers already hold the app's live coherent writes in - // host-visible storage carrying every usage; bind directly, no re-upload/staging. - if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) { - resource->lastUseSerial = m_frameSerial; - outSlice = resource->buffer.GetSlice(0, size); - return outSlice.IsValid(); - } - - const Bool needsRecreate = !resource->buffer.IsValid() || resource->storageSize != size || - ((resource->usageFlags & requiredUsage) != requiredUsage) || - resource->pendingFullUpload; - if (needsRecreate) { - const VkBufferUsageFlags usage = resource->usageFlags | requiredUsage; - DeferRelease(std::move(resource->buffer)); - if (!CreateResidentStorage(*resource, size, usage)) { - return false; - } - if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) { - MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed"); - resource->buffer.Destroy(); - resource->storageSize = 0; - resource->usageFlags = 0; - return false; - } - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, - static_cast(size)); - } - resource->pendingFullUpload = false; - } - - resource->lastUseSerial = m_frameSerial; - outSlice = resource->buffer.GetSlice(0, size); - return true; - } - - Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind, - const SharedPtr& bufferObject, - BufferSlice& outSlice) { - (void)kind; - MOBILEGL_ASSERT(bufferObject != nullptr, "VkBufferManager::AcquireStreamedSlice requires valid buffer object"); - - auto resource = GetOrCreateResource(bufferObject); - bufferObject->SyncPersistentMappedRange(); - - // A persistently mapped resource's storage IS the application's copy of the bytes - - // the frontend adopted it in place of the shadow and hands out pointers into it, and - // a shader can have written bytes the shadow never saw (a transform feedback - // capture). Streaming a second copy would feed this draw the stale shadow, and the - // downgrade below would release the storage the application still points at, - // breaking the "never recreated" promise AcquirePersistentMap makes. - if (resource->persistentMapped) { - return AcquireResidentSlice(kind, bufferObject, outSlice); - } - - const VkDeviceSize size = static_cast(bufferObject->GetSize()); - if (size == 0) { - MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero"); - return false; - } - - const Uint64 changeSerial = bufferObject->GetChangeSerial(); - if (resource->transientFrameSerial == m_frameSerial && resource->transientChangeSerial == changeSerial && - resource->transientSize == size && resource->transientSlice.IsValid()) { - outSlice = resource->transientSlice; - 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) { - // Promotion moves the buffer off the arena and onto resident storage. - resource->promotedResident = true; - resource->promotedChangeSerial = changeSerial; - BumpSliceEpoch(*resource); - if (AcquireResidentSlice(kind, bufferObject, outSlice)) { - return true; - } - resource->promotedResident = false; // resident creation failed: stream as before - } - } else { - resource->unchangedStreak = 0; - } - - // A fresh arena allocation: a different slice than the last call handed back, - // and (below) the point where a promoted buffer's resident storage is released. - // The stable-promotion exit above returns before this, so a buffer the app has - // stopped touching keeps one slice for as long as it keeps its resident storage. - BumpSliceEpoch(*resource); - if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16, - outSlice)) { - return false; - } - if (MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast(size)); - } - resource->transientSlice = outSlice; - resource->transientFrameSerial = m_frameSerial; - resource->transientChangeSerial = changeSerial; - resource->transientSize = size; - - // Streaming path is authoritative now; release resident storage so we do - // not keep a second, stale copy alive (downgrade). - if (resource->buffer.IsValid()) { - DeferRelease(std::move(resource->buffer)); - resource->storageSize = 0; - } - return true; - } - - void VkBufferManager::DeferRelease(VkBufferObject&& buffer) { - if (!buffer.IsValid()) { - return; - } - - if (m_deferredBufferReleases.empty()) { - buffer.Destroy(); - return; - } - - MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredBufferReleases.size(), - "VkBufferManager::DeferRelease current frame index out of range"); - m_deferredBufferReleases[m_currentFrameIndex].push_back(std::move(buffer)); - } - void VkBufferManager::CollectDeferredReleases(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_deferredBufferReleases.size(), "VkBufferManager::CollectDeferredReleases frame index out of range"); m_deferredBufferReleases[frameIndex].clear(); - m_deferredResourceReleases[frameIndex].clear(); // The wire list is not per-slot and does not wait for a frame boundary; a boundary is // just one more point to sweep at (BeginFrame after its slot fence, and every slot of // the idle drain, where the renderer-idle rule empties the list). @@ -2035,34 +1431,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { return m_unboundTexelBuffer.GetSlice(); } - VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) { - switch (kind) { - case BufferKind::Vertex: - case BufferKind::Index: - // A GL buffer can be rebound between ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER, - // and may even be used as both within the same draw setup. Keep resident - // vertex/index buffers compatible with both roles from the start so we - // never need to recreate a buffer after it has already been bound. - return VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; - case BufferKind::Uniform: - return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; - case BufferKind::TextureBuffer: - // Both texel roles, for the same reason vertex/index carry both bits: one GL buffer - // texture can be read as a samplerBuffer and written as an imageBuffer, and which of - // the two it is only becomes known when a shader that uses it is bound - long after - // the resident buffer was created. A VkBufferView for a storage-texel descriptor is - // invalid unless the buffer was created with the storage bit, so a buffer that - // acquired only the uniform bit could never be given one. - return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT; - case BufferKind::ShaderStorage: - return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; - case BufferKind::Indirect: - return VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - default: - return 0; - } - } - void VkBufferManager::DestroyAllDeferredReleases() { // Both callers (Shutdown, RecreateTransientArenas) have proven the device idle. DestroyAllDeferredWireReleases(); @@ -2073,12 +1441,5 @@ namespace MobileGL::MG_Backend::DirectVulkan { releases.clear(); } m_deferredBufferReleases.clear(); - for (auto& releases : m_deferredResourceReleases) { - for (auto& resource : releases) { - resource->buffer.Destroy(); - } - releases.clear(); - } - m_deferredResourceReleases.clear(); } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 89b2aed0..7c3f1f8d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -38,56 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool transformFeedbackUsageEnabled = false; }; - // The DirectVulkan storage behind one frontend buffer (pipe_resource analogue). - // Owned (refcounted) by the frontend BufferObject; the manager holds only weak - // references (for shutdown) plus strong references on deferred-release lists. - class VkBufferResource : public MG_State::GLState::BackendBufferResource { - public: - ~VkBufferResource() override = default; - - // Resident storage (may be invalid for streaming-only buffers). - VkBufferObject buffer; - VkDeviceSize storageSize = 0; - VkBufferUsageFlags usageFlags = 0; - // Frame serial of the last GPU reference; drives busy tracking. - Uint64 lastUseSerial = 0; - // Set when an immediate op could not be applied; forces a full re-upload - // on the next AcquireResidentSlice. - Bool pendingFullUpload = false; - // Backs a zero-copy coherent persistent map (PipeResource GPU residency): the - // buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is - // never orphaned or recreated. Draw-time acquire binds it directly, no re-upload. - Bool persistentMapped = false; - - // Bumped from a manager-wide counter every time anything that decides which - // BufferSlice an Acquire*Slice call hands back changes: storage created or - // released, a full re-upload becoming due, a promotion/demotion between - // resident and streamed storage, or a new per-frame arena slice. Callers that - // memoise a resolved slice compare this to prove the memo still describes the - // buffer. The counter is manager-wide (never per-resource) so a freshly - // created resource - including one that replaces a destroyed resource at the - // same address - can never reproduce a value some memo already holds. 0 means - // "no slice has ever been handed out", which no memo can match. - Uint64 sliceEpoch = 0; - - // Cached transient (streaming) slice for the current frame. - BufferSlice transientSlice{}; - 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, // for staged buffer-range copies. Implemented by VulkanRenderer. class IBufferCopyCommandProvider { @@ -198,36 +148,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // UniformManager::AcquireUnboundTexelBufferView. BufferSlice AcquireUnboundTexelBufferDescriptor(); - // Draw-time acquire for resident (device-storage) buffers: ensures the - // resource exists and is fully uploaded, marks it used this frame. - Bool AcquireResidentSlice(BufferKind kind, const SharedPtr& bufferObject, - BufferSlice& outSlice); - // Draw-time acquire for streamed buffers: uploads the whole shadow into - // the per-frame arena (cached by change serial), releasing any resident - // storage the buffer may still own. - Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr& bufferObject, - BufferSlice& outSlice); - - // Zero-copy persistent map (PipeResource GPU residency): create (once) a - // HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage, - // seed it from the shadow, and return its mapped base for the app to write into - // directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow). - void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject); - - // Immediate ops, dispatched from the frontend BufferBackendOps table. - void OnRespecify(MG_State::GLState::BufferObject& bufferObject); - void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size); - void OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, DataPtr data); - void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range, - Flags appAccess); - void OnResourceDestroyed(SharedPtr&& resource); - Uint64 GetFrameSerial() const { return m_frameSerial; } - // Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a - // memo was taken means no buffer this manager owns changed which slice it hands - // back, and none was persistently mapped, in between - so a memo of resolved - // slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half. - Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; } // Bumped every time this manager destroys a WIRE store's VkBuffer (see // m_wireStoreDestroyEpoch). Unchanged since a memo was taken means no VkBuffer handle // that memo names can have been freed and re-minted in between, which is the one @@ -237,16 +158,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // below it may be considered signaled. Drives IsResourceBusy and the // backend GL fence objects. Uint64 GetCompletedSerial() const; - // Busy = potentially referenced by GPU work that has not been fenced yet - // (including commands recorded for the current, unsubmitted frame). - Bool IsResourceBusy(const VkBufferResource& resource) const; - // Hand the manager a buffer to destroy once the frame that recorded commands - // naming it has completed. The renderer's blit path needs a device-local - // scratch store for one recorded operation, which cannot be a stack local: - // the glBlitFramebuffer that records the commands returns long before the - // command buffer is submitted. - void DeferRelease(VkBufferObject&& buffer); - private: struct WireBufferResource { VkBufferObject buffer; @@ -328,8 +239,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // while a memo still maps that handle to a descriptor set baked to the dead store's // memory: silent wrong bytes, no Fatal (ID-P7-43). m_wireStoreDestroyEpoch is the // fact the memos fold in: every wire-store destroy bumps it, so a memo taken before - // the destroy cannot match after it. Deliberately NOT m_sliceEpochCounter, which every - // WriteWireBuffer bumps and which would defeat the memo on every glBufferSubData. + // the destroy cannot match after it. Deliberately not bumped by WriteWireBuffer, which + // would defeat the memo on every glBufferSubData. struct DeferredWireRelease { VkBufferObject buffer; Uint64 lastUseSerial = 0; @@ -368,25 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { void NoteWireStorePeaks(); void PublishWireReclaimGauges(); Bool InitializeTransientArenas(); - static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); - VkBufferResource* GetOrCreateResource(const SharedPtr& bufferObject); - static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject); - Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage, - VkMemoryPropertyFlags requiredFlags = 0); - // Swap storage (conditional orphan) and refill it from the shadow copy. - Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject); - // Record a staging-slice copy into the resident storage, ordered against - // in-flight and already-recorded GPU work. - Bool StagedRangeCopy(VkBufferResource& resource, const void* data, - SizeT offset, SizeT size); Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size); void CollectDeferredReleases(Uint32 frameIndex); void DestroyAllDeferredReleases(); - void TrackLiveResource(const SharedPtr& resource); - void ReleaseAllLiveResources(); - // See VkBufferResource::sliceEpoch. - void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; } - VkBufferManagerInitInfo m_initInfo{}; BufferArena m_transientUploadArena; // See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down @@ -396,8 +291,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkBufferObject m_unboundTexelBuffer; IBufferCopyCommandProvider* m_copyProvider = nullptr; Vector> m_deferredBufferReleases; - Vector>> m_deferredResourceReleases; - Vector> m_liveResources; std::unordered_map m_wireBuffers; // See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above: // the whole point is that these entries do not wait for a frame slot to come round. @@ -413,17 +306,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_wireDeferredSyncs = 0; // See GetWireStoreDestroyEpoch and the DeferredWireRelease comment. Bumped on every // path that destroys a wire store's VkBuffer, and never reset (not even by Shutdown), - // for m_sliceEpochCounter's reason: a memo taken before a re-initialize must not match + // so that a memo taken before a re-initialize must not match // a handle minted after it. Uint64 m_wireStoreDestroyEpoch = 0; - // Size m_liveResources had just after the last sweep; the next sweep waits for it to double. - SizeT m_liveResourcesLastPruned = 0; Uint32 m_currentFrameIndex = 0; Uint64 m_frameSerial = 1; Uint64 m_completedSerialFloor = 0; - // Never reset (not even by Shutdown): a value handed to a resource must stay - // unique for the process, or a memo taken before a re-initialize could match - // a different resource's state after it. - Uint64 m_sliceEpochCounter = 0; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp index 45def086..ca7d1126 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp @@ -68,13 +68,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { toLinear(payload.color.z()), payload.color.w()); } - static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) { - if (!IsCubeMapFaceUploadTarget(target)) { - return 0; - } - return static_cast(target) - static_cast(TextureUploadTarget::CubeMapPositiveX); - } - // ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z() // raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is // not a cosmetic difference: the count below is not key-only, it is written straight into @@ -83,15 +76,4 @@ namespace MobileGL::MG_Backend::DirectVulkan { // for good, while the very same queued clear cleared all six through the render pass's // LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent. - Bool VkClearManager::Initialize() { - return true; - } - - void VkClearManager::Shutdown() { - const std::lock_guard lock(m_mutex); - m_pendingClears.clear(); - m_aliveObjects.clear(); - m_pendingCount.store(static_cast(m_pendingClears.size()), std::memory_order_relaxed); - } - } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h index ccc1162c..7a2688ad 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h @@ -55,79 +55,4 @@ namespace MobileGL::MG_Backend::DirectVulkan { // encoding itself. void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat); - struct PendingClearKey { - MG_State::GLState::ITextureObject* texture = nullptr; - Uint64 textureLifetimeId = 0; - Uint32 mipLevel = 0; - Uint32 baseArrayLayer = 0; - Uint32 layerCount = 1; - - Bool operator==(const PendingClearKey& other) const { - return texture == other.texture && textureLifetimeId == other.textureLifetimeId && - mipLevel == other.mipLevel && - baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount; - } - }; - - struct TextureIdentity { - MG_State::GLState::ITextureObject* texture = nullptr; - Uint64 lifetimeId = 0; - - Bool operator==(const TextureIdentity& other) const { - return texture == other.texture && lifetimeId == other.lifetimeId; - } - }; - - struct PendingClearEntry { - PendingClearKey key{}; - ClearAttachmentPayload payload{}; - }; - - struct PendingClearKeyHash { - SizeT operator()(const PendingClearKey& key) const { - const SizeT textureHash = std::hash{}(key.texture); - const SizeT textureLifetimeHash = std::hash{}(key.textureLifetimeId); - const SizeT mipHash = std::hash{}(key.mipLevel); - const SizeT layerHash = std::hash{}(key.baseArrayLayer); - const SizeT layerCountHash = std::hash{}(key.layerCount); - SizeT hash = textureHash; - hash ^= textureLifetimeHash + 0x9e3779b9u + (hash << 6) + (hash >> 2); - hash ^= mipHash + 0x9e3779b9u + (hash << 6) + (hash >> 2); - hash ^= layerHash + 0x9e3779b9u + (hash << 6) + (hash >> 2); - hash ^= layerCountHash + 0x9e3779b9u + (hash << 6) + (hash >> 2); - return hash; - } - }; - - struct TextureIdentityHash { - SizeT operator()(const TextureIdentity& key) const { - SizeT hash = std::hash{}(key.texture); - hash ^= std::hash{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2); - return hash; - } - }; - - class VkClearManager { - public: - Bool Initialize(); - void Shutdown(); - - private: - Uint8 m_gcCounter = 0; - public: - // Lock-free probe for the consecutive-draw fast path: any pending clear - // forces the full SetupDraw path (which materializes/consumes it). - Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; } - - private: - mutable std::mutex m_mutex; - // Lock-free mirror of m_pendingClears.size(), maintained under m_mutex - // by every mutation. The per-draw probes (HasPendingClear/GetPending*) - // read it before taking the lock: during draw batches the pending set - // is almost always empty, so this turns several locked map probes per - // draw into one relaxed load. - std::atomic m_pendingCount{0}; - std::unordered_map m_pendingClears; - std::unordered_map, TextureIdentityHash> m_aliveObjects; - }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 54c8130a..cb7c7f6b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -54,38 +54,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return {attachmentExtent.x(), attachmentExtent.y()}; } - void VkRenderPassManager::RenderbufferResource::Destroy(VkDevice device, VmaAllocator allocator) { - if (view != VK_NULL_HANDLE) { - vkDestroyImageView(device, view, nullptr); - } - if (unormTwinView != VK_NULL_HANDLE) { - vkDestroyImageView(device, unormTwinView, nullptr); - } - if (image != VK_NULL_HANDLE && allocation != nullptr) { - vmaDestroyImage(allocator, image, allocation); - } - renderbuffer.reset(); - image = VK_NULL_HANDLE; - allocation = nullptr; - view = VK_NULL_HANDLE; - unormTwinView = VK_NULL_HANDLE; - layout = VK_IMAGE_LAYOUT_UNDEFINED; - format = VK_FORMAT_UNDEFINED; - aspect = VK_IMAGE_ASPECT_NONE; - extent = {0, 0}; - sampleCount = VK_SAMPLE_COUNT_1_BIT; - internalFormat = TextureInternalFormat::Unknown; - samples = 0; - deadSinceFrame = kNeverObservedDead; - } - VkRenderPassManager::VkRenderPassManager(VkDevice device, VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config, - VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject): + VkTextureManager& textureManager, SwapchainObject& swapchainObject): m_device(device), m_physicalDevice(physicalDevice), m_allocator(allocator), m_config(config), - m_clearManager(clearManager), m_textureManager(textureManager), m_swapchainObject(swapchainObject) { + m_textureManager(textureManager), m_swapchainObject(swapchainObject) { RenderPassEntry::s_device.Get() = m_device; - s_clearManager.Get() = &m_clearManager; s_textureManager.Get() = &m_textureManager; s_swapchainObject.Get() = &m_swapchainObject; s_renderPassManager.Get() = this; @@ -98,146 +72,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkRenderPassManager::Shutdown() { - m_renderPasses.clear(); - for (auto& [_, resource] : m_renderbufferResources) { - resource.Destroy(m_device, m_allocator); - } - m_renderbufferResources.clear(); - CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle - m_pendingRenderbufferClears.clear(); - RenderPassEntry::s_textureResourcesScratch.clear(); s_activeRenderPass.Get() = {}; s_hasActiveRenderPass.Get() = false; } - Uint64 VkRenderPassManager::RetireAgeFrames() const { - // MaxFramesInFlight + 2 covers the frame ring plus one boundary for the - // recording-to-submit gap and one because OnPresent runs ahead of Present's - // fence wait; the floor of 8 keeps a margin over the default ring of 3 while - // still releasing multi-MB attachment memory promptly (the render-pass cache's - // 1024-frame retirement would pin it for no additional safety). - return std::max(8, static_cast(m_config.MaxFramesInFlight) + 2); - } - - void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) { - // The superseded backing may still be referenced by in-flight command buffers - // (glRenderbufferStorage can respecify a renderbuffer drawn this very frame), - // so it is parked and destroyed only after RetireAgeFrames() boundaries. - if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) { - return; - } - m_deferredRenderbufferReleases.push_back( - {resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter}); - resource.image = VK_NULL_HANDLE; - resource.allocation = nullptr; - resource.view = VK_NULL_HANDLE; - resource.unormTwinView = VK_NULL_HANDLE; - } - - void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) { - if (m_deferredRenderbufferReleases.empty()) { - return; - } - const Uint64 retireAgeFrames = RetireAgeFrames(); - std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) { - if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) { - return false; - } - if (release.view != VK_NULL_HANDLE) { - vkDestroyImageView(m_device, release.view, nullptr); - } - if (release.unormTwinView != VK_NULL_HANDLE) { - vkDestroyImageView(m_device, release.unormTwinView, nullptr); - } - if (release.image != VK_NULL_HANDLE) { - vmaDestroyImage(m_allocator, release.image, release.allocation); - } - return true; - }); - } - - void VkRenderPassManager::CollectRenderbufferGarbage() { - // Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by - // command buffers submitted up to frames-in-flight frames ago (it was legally - // attached and drawn right up to its deletion), so the first observation of an - // expired weak reference only stamps the current frame counter; Destroy runs once - // enough frame boundaries have passed that the stamping frame's submission fence - // has provably been waited (see RetireAgeFrames). - const Uint64 retireAgeFrames = RetireAgeFrames(); - for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) { - auto& resource = it->second; - const auto liveRenderbuffer = resource.renderbuffer.lock(); - if (liveRenderbuffer && liveRenderbuffer.get() == it->first) { - resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead; - ++it; - continue; - } - if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) { - resource.deadSinceFrame = m_frameCounter; - ++it; - continue; - } - if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) { - ++it; - continue; - } - m_pendingRenderbufferClears.erase(it->first); - resource.Destroy(m_device, m_allocator); - it = m_renderbufferResources.erase(it); - } - } - - void VkRenderPassManager::PurgeRenderPasses() { - Vector destroyedRenderPasses; - destroyedRenderPasses.reserve(m_renderPasses.size()); - for (auto& [_, entry] : m_renderPasses) destroyedRenderPasses.push_back(entry.renderPass); - m_renderPasses.clear(); - if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) { - m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses); - } - } - - void VkRenderPassManager::OnPresent() { - ++m_frameCounter; - - // Runs every frame boundary, ahead of the render-pass sweep gate below: the walk - // is O(#renderbuffer resources) — single digits in practice — and per-frame - // invocation keeps dead-resource reclaim latency at the aging bound instead of - // coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call - // site never runs again once an app stops using renderbuffers). - CollectRenderbufferGarbage(); - CollectDeferredRenderbufferReleases(/*destroyAll=*/false); - - // Sweep occasionally; evict entries whose last use is far past every - // in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed - // safely (RenderPassEntry's destructor releases the handles). - constexpr Uint64 kSweepInterval = 256; - constexpr Uint64 kRetireAgeFrames = 1024; - if ((m_frameCounter % kSweepInterval) != 0) { - return; - } - - // Collect the dying handles and notify once after the loop: pipelines hashed - // on them share the entries' >kRetireAgeFrames idleness (they are only bound - // by draws that hit those entries), so the observer may destroy them - // immediately - and a single batched notification costs one pipeline-cache - // scan instead of one per evicted pass. - Vector destroyedRenderPasses; - const Uint64 activeHash = s_hasActiveRenderPass.Get() ? s_activeRenderPass.Get().hash : 0; - for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) { - const Bool isActive = s_hasActiveRenderPass.Get() && it->first == activeHash; - if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) { - destroyedRenderPasses.push_back(it->second.renderPass); - it = m_renderPasses.erase(it); - } else { - ++it; - } - } - if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) { - m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses); - } - } - Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) { // TODO: Transition all the attachments into proper layout before starting the render pass VkRenderPassBeginInfo renderPassBeginInfo; @@ -258,21 +96,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { renderPassBeginInfo.pClearValues = clearValues.data(); vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); - // Pre-pass stream bookkeeping: this pass's attachment images are now - // referenced by the open frame recording. - if (s_textureManager.Get() != nullptr) { - for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) { - if (tracked.target == TrackedAttachmentTarget::Texture) { - if (const auto texture = tracked.texture.lock()) { - s_textureManager.Get()->StampTextureRecordingUse(texture.get()); - } - } - } - } s_activeRenderPass.Get().commandBuffer = commandBuffer; s_activeRenderPass.Get().hash = renderPassEntry.hash; s_activeRenderPass.Get().compatibilityHash = renderPassEntry.compatibilityHash; - s_activeRenderPass.Get().trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts; s_activeRenderPass.Get().extent = renderPassEntry.extent; s_hasActiveRenderPass.Get() = true; @@ -281,59 +107,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool VkRenderPassManager::EndRenderPass(VkCommandBuffer commandBuffer) { BumpWireImageWriteEpoch(); - auto* activeRenderPass = GetActiveRenderPass(); - Vector trackedAttachmentLayouts; - if (activeRenderPass != nullptr) { - trackedAttachmentLayouts = Move(activeRenderPass->trackedAttachmentLayouts); - } vkCmdEndRenderPass(commandBuffer); - // Layout reconciliation below may record barriers, whose guard closes an active - // pass. Detach the ended pass first so that guard cannot recursively end it again. s_activeRenderPass.Get() = {}; s_hasActiveRenderPass.Get() = false; - if (!trackedAttachmentLayouts.empty()) { - for (const auto& trackedAttachment : trackedAttachmentLayouts) { - switch (trackedAttachment.target) { - case TrackedAttachmentTarget::Texture: - MOBILEGL_ASSERT(s_textureManager.Get() != nullptr, "EndRenderPass: texture manager is null"); - if (const auto texture = trackedAttachment.texture.lock()) { - s_textureManager.Get()->UpdateTrackedImageLayoutAfterAttachmentWrite( - commandBuffer, - texture.get(), - trackedAttachment.textureMipLevel, - trackedAttachment.finalLayout); - } - break; - case TrackedAttachmentTarget::Renderbuffer: - MOBILEGL_ASSERT(s_renderPassManager.Get() != nullptr, "EndRenderPass: render pass manager is null"); - if (const auto renderbuffer = trackedAttachment.renderbuffer.lock()) { - auto resourceIt = - s_renderPassManager.Get()->m_renderbufferResources.find(renderbuffer.get()); - if (resourceIt != s_renderPassManager.Get()->m_renderbufferResources.end()) { - resourceIt->second.layout = trackedAttachment.finalLayout; - } - } - break; - case TrackedAttachmentTarget::SwapchainColor: - MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null"); - s_swapchainObject.Get()->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout); - // The pass stored into the attachment: its content is defined - // until the image is next presented. - s_swapchainObject.Get()->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true); - break; - case TrackedAttachmentTarget::SwapchainDepthStencil: - MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null"); - s_swapchainObject.Get()->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex, - trackedAttachment.finalLayout); - s_swapchainObject.Get()->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true); - break; - default: - MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d", - static_cast(trackedAttachment.target)); - break; - } - } - } return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 35db1b65..da47257e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -20,40 +20,6 @@ #include namespace MobileGL::MG_Backend::DirectVulkan { - enum class TrackedAttachmentTarget : Uint8 { - Texture, - Renderbuffer, - SwapchainColor, - SwapchainDepthStencil - }; - - struct PendingClearAttachmentInfo { - // Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space). - Uint32 attachmentIndex = 0; - // Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL - // draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete. - // Only meaningful for color clears. - Uint32 colorAttachmentSlot = 0; - PendingClearKey key{}; - MG_State::GLState::RenderbufferObject* renderbuffer = nullptr; - Bool hasInlinePayload = false; - ClearAttachmentPayload inlinePayload{}; - }; - - struct TrackedAttachmentLayoutInfo { - TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture; - WeakPtr texture; - // Identity-compare shortcut for the per-draw "does the active pass use - // this sampled texture" probe: comparing this against a LIVE texture's - // address needs no weak_ptr::lock (two refcount atomics per probe). - // May dangle once the texture dies - compare only, never dereference. - MG_State::GLState::ITextureObject* textureRaw = nullptr; - WeakPtr renderbuffer; - Uint32 textureMipLevel = 0; - Uint32 swapchainImageIndex = 0; - VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED; - }; - struct DepthStencilAttachmentLoadInfo { VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; @@ -68,13 +34,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { struct RenderPassEntry { // Per Magma session (MagmaSession.h): each session's renderer is its own device. static inline SessionLocal s_device; - static inline thread_local Vector s_textureResourcesScratch; Uint64 hash = 0; VkRenderPass renderPass = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE; Uint64 compatibilityHash = 0; - Vector pendingClearAttachments; - Vector trackedAttachmentLayouts; Uint32 attachmentCount = 0; Uint32 colorAttachmentCount = 0; Bool hasDepthStencilAttachment = false; @@ -92,8 +55,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::swap(renderPass, that.renderPass); std::swap(framebuffer, that.framebuffer); std::swap(compatibilityHash, that.compatibilityHash); - std::swap(pendingClearAttachments, that.pendingClearAttachments); - std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts); std::swap(attachmentCount, that.attachmentCount); std::swap(colorAttachmentCount, that.colorAttachmentCount); std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment); @@ -126,8 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::swap(renderPass, that.renderPass); std::swap(framebuffer, that.framebuffer); std::swap(compatibilityHash, that.compatibilityHash); - std::swap(pendingClearAttachments, that.pendingClearAttachments); - std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts); std::swap(attachmentCount, that.attachmentCount); std::swap(colorAttachmentCount, that.colorAttachmentCount); std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment); @@ -138,32 +97,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } return *this; } - RenderPassEntry( - Uint64 hash, - VkRenderPass renderpass, - VkFramebuffer framebuffer, - Uint64 compatibilityHash, - const Vector& pendingClearAttachments, - const Vector& trackedAttachmentLayouts, - Uint32 attachmentCount, - Uint32 colorAttachmentCount, - Bool hasDepthStencilAttachment, - VkSampleCountFlagBits sampleCount, - IntVec2 extent, Uint32 layers): - hash(hash), - renderPass(renderpass), - framebuffer(framebuffer), - compatibilityHash(compatibilityHash), - pendingClearAttachments(Move(pendingClearAttachments)), - trackedAttachmentLayouts(Move(trackedAttachmentLayouts)), - attachmentCount(attachmentCount), - colorAttachmentCount(colorAttachmentCount), - hasDepthStencilAttachment(hasDepthStencilAttachment), - sampleCount(sampleCount), - extent(extent), - layers(layers) - {} - ~RenderPassEntry() { if (renderPass != VK_NULL_HANDLE) { vkDestroyRenderPass(s_device.Get(), renderPass, nullptr); @@ -186,7 +119,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 hash = 0; Uint64 compatibilityHash = 0; VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - Vector trackedAttachmentLayouts; IntVec2 extent = {0, 0}; Bool CompatibleWith(const RenderPassEntry& that) const { @@ -202,30 +134,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { public: using HashType = Uint64; - // Notified once per OnPresent sweep with every aged-out entry's VkRenderPass - // value: pipelines are hashed on the raw handle, and once destroyed the value - // may be recycled for an incompatible pass, so dependent caches must purge - // everything keyed on them before any new pass can be created (the sweep and - // the notification run back-to-back with no creation in between; observers - // compare the values, never dereference them). Batched so a mass-idle cohort - // (shader-pack switch, dimension exit) costs the observer one pipeline-cache - // scan, not one per dying pass. The wholesale paths - // (Shutdown/RecreateSwapchain) do not notify - their callers already drop - // every pipeline outright. - class IEvictionObserver { - public: - virtual ~IEvictionObserver() = default; - virtual void OnRenderPassesDestroyed(const Vector& renderPasses) = 0; - }; - VkRenderPassManager(VkDevice device, VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config, - VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject); + VkTextureManager& textureManager, SwapchainObject& swapchainObject); ~VkRenderPassManager(); - // Observer may be null (no notifications). Not owned. - void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; } - Bool Initialize(); void Shutdown(); // The surface target the default framebuffer currently names (VulkanRenderer's surface @@ -233,13 +146,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { void SetDefaultFramebufferTarget(Uint64 serial) { m_defaultFramebufferTarget = serial; } - // Destroys every cached pass and framebuffer (not the renderbuffers). The device must be - // idle and no pass may be open: a surface target's image views are going away. - void PurgeRenderPasses(); - - // Frame boundary hook: ages the render-pass cache and evicts long-unused - // entries (their command buffers retired many frames ago). - void OnPresent(); static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry); static Bool EndRenderPass(VkCommandBuffer commandBuffer); static ActiveRenderPassInfo* GetActiveRenderPass(); @@ -248,13 +154,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VmaAllocator m_allocator = nullptr; const VulkanRendererConfig& m_config; - VkClearManager& m_clearManager; VkTextureManager& m_textureManager; SwapchainObject& m_swapchainObject; - UnorderedMap m_renderPasses; - // Monotonic frame counter (bumped in OnPresent) for render-pass cache aging. - Uint64 m_frameCounter = 0; - IEvictionObserver* m_evictionObserver = nullptr; // Bumped whenever a renderbuffer VkImage is (re)created; together with the texture // manager's image epoch this invalidates the render-pass fast path on any attachment @@ -270,95 +171,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_defaultFramebufferTarget = 0; - public: - struct RenderbufferResource { - // deadSinceFrame sentinel: the owning weak reference has not been observed - // expired. Dead resources age past every in-flight frame before Destroy - // (see CollectRenderbufferGarbage); the GPU may still reference the image - // for frames-in-flight frames after the GL object dies. - static constexpr Uint64 kNeverObservedDead = UINT64_MAX; - - WeakPtr renderbuffer; - VkImage image = VK_NULL_HANDLE; - VmaAllocation allocation = nullptr; - VkImageView view = VK_NULL_HANDLE; - // UNORM reinterpretation of an sRGB image, used as the attachment view while - // GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats. - VkImageView unormTwinView = VK_NULL_HANDLE; - VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; - VkFormat format = VK_FORMAT_UNDEFINED; - VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; - VkExtent2D extent = {0, 0}; - VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; - TextureInternalFormat internalFormat = TextureInternalFormat::Unknown; - Int samples = 0; - // m_frameCounter value at which the weak reference was first seen expired. - Uint64 deadSinceFrame = kNeverObservedDead; - - void Destroy(VkDevice device, VmaAllocator allocator); - }; - - private: - struct PendingRenderbufferClear { - WeakPtr renderbuffer; - ClearAttachmentPayload payload{}; - }; - - // A superseded renderbuffer backing (glRenderbufferStorage respecify) parked - // until enough frame boundaries have passed that no in-flight command buffer - // can still reference it; destroyed in OnPresent (see RetireAgeFrames). - struct DeferredRenderbufferRelease { - VkImage image = VK_NULL_HANDLE; - VmaAllocation allocation = nullptr; - VkImageView view = VK_NULL_HANDLE; - VkImageView unormTwinView = VK_NULL_HANDLE; - Uint64 deferredAtFrame = 0; - }; - - // Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap: - // callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further - // calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and - // destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then - // materializes the source's pending clear, which looks that same resource up again. Growing - // an open-addressed table relocates every element, so the cached pointer went on to name - // freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed - // out at "source image layout is undefined", silently dropping the blit - - // renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the - // iterations that grew the table. - // - // Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover - // this: the destination resolve still runs after the source pointer is taken. The depth blit, - // GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same - // kind of pointer, so the invariant belongs in the container rather than in a per-call-site - // ordering rule. m_textureResources is node-based for the same reason. - // - // The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read - // the paragraph above as merely historical: ska erases by shifting the rest of the probe - // cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers' - // entries - a cached pointer can now be invalidated by a key it has nothing to do with, which - // no call-site ordering rule can defend against. (What did change: ska's operator[] returns on - // a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates. - // That narrows the insert hazard; it does not touch the erase one.) - std::unordered_map m_renderbufferResources; - UnorderedMap m_pendingRenderbufferClears; - Vector m_deferredRenderbufferReleases; // Supported sample counts per attachment format, so per-draw resource lookups // do not repeat vkGetPhysicalDeviceImageFormatProperties. UnorderedMap m_attachmentSampleCountsByFormat; - void CollectRenderbufferGarbage(); - // Frame-boundary margin after which a resource last referenced by a retired - // GL object (or superseded backing) is provably past every in-flight frame. - Uint64 RetireAgeFrames() const; - void DeferRenderbufferBackingRelease(RenderbufferResource& resource); - void CollectDeferredRenderbufferReleases(Bool destroyAll); static inline thread_local XXH64_state_t* m_hashState = XXH64_createState(); // Per Magma session (MagmaSession.h): one session's open render pass and managers are not // another's. static inline SessionLocal s_activeRenderPass; static inline SessionLocal s_hasActiveRenderPass; - static inline SessionLocal s_clearManager; static inline SessionLocal s_textureManager; static inline SessionLocal s_swapchainObject; static inline SessionLocal s_renderPassManager; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index fd4c37cf..1c1dc917 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -228,30 +228,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity( - MG_State::GLState::ITextureObject* texture) { - // A GL texture view (ARB_texture_view) is identified by the texture whose STORAGE it - // views, not by itself. Everything this identity keys - the TextureResource, the tracked - // image layout, the alive-object weak reference, the storage-usage marks, the per-draw - // sync memos - is a property of the IMAGE, and a view shares that image exactly. Doing - // the resolution here rather than at each call site is what makes it impossible to miss - // one: a layout update posted against a view's own identity would have found no resource - // at all, which is precisely how an attached view came back blank. - // - // One hop suffices and cannot recurse: glTextureView composes a view-of-a-view onto the - // root at creation, so a storage owner is never itself a view. - if (texture != nullptr) { - const auto& storageOwner = texture->GetViewStorageOwner(); - if (storageOwner) { - texture = storageOwner.get(); - } - } - return TextureIdentity{ - .texture = texture, - .lifetimeId = texture ? texture->GetLifetimeId() : 0, - }; - } - static void GetImageTransitionDestinationState(VkImageLayout newLayout, VkPipelineStageFlags& outDstStageMask, VkAccessFlags& outDstAccessMask) { @@ -551,13 +527,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } DestroyDeferredReleases(); ++m_resourceEraseEpoch; // every memoized resource pointer dies with the map - m_textureResources.clear(); // Destroy images while the device/allocator still exist, including after context death. m_wireTextureResources.clear(); m_wireRenderbufferResources.clear(); m_sharedImageUses.clear(); - m_aliveObjects.clear(); - m_storageImageTextures.clear(); m_device = VK_NULL_HANDLE; m_physicalDevice = VK_NULL_HANDLE; @@ -599,118 +572,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) { - m_viewRequestedImageFlags.erase(identity); - m_viewRequestedFormats.erase(identity); - auto resourceIt = m_textureResources.find(identity); - if (resourceIt != m_textureResources.end()) { - DeferResourceRelease(Move(resourceIt->second)); - m_textureResources.erase(resourceIt); - } - m_aliveObjects.erase(identity); - m_storageImageTextures.erase(identity); - // Invalidate every cross-draw sampled-texture memo: the erased - // resource's address may be reused by a future emplace. - ++m_resourceEraseEpoch; - } - - void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) { - if (texture == nullptr) { - return; - } - - Vector staleAliases; - for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) { - if (it->first.texture != texture) { - continue; - } - const auto liveTexture = it->second.lock(); - if (!liveTexture || liveTexture.get() != texture || - liveTexture->GetLifetimeId() != it->first.lifetimeId) { - staleAliases.emplace_back(it->first); - } - } - for (const auto& identity : staleAliases) { - EraseTrackedTexture(identity); - } - } - - void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) { - if (texture == nullptr) { - return; - } - auto it = m_textureResources.find(MakeTextureIdentity(texture)); - if (it != m_textureResources.end()) { - it->second.lastRecordingGeneration = m_recordingGeneration; - } - } - - void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) { - if (texture == nullptr) { - return; - } - auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(*texture))); - if (it != m_textureResources.end()) { - StampResourceRecordingWrite(it->second); - } - } - - void VkTextureManager::UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, - MG_State::GLState::ITextureObject* texture, - Uint32 writtenMipLevel, - VkImageLayout newLayout) { - MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayoutAfterAttachmentWrite: texture is null"); - auto it = m_textureResources.find(MakeTextureIdentity(texture)); - MOBILEGL_ASSERT(it != m_textureResources.end(), - "UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d has no tracked resource", - texture->GetExternalIndex()); - - auto& resource = it->second; - MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, - "UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d has null image", - texture->GetExternalIndex()); - MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels, - "UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u", - texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels); - // Pre-pass stream bookkeeping: the render pass that just ended wrote this image. - StampResourceRecordingWrite(resource); - - if (resource.layout != newLayout && resource.mipLevels > 1) { - VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - VkAccessFlags srcAccessMask = 0; - GetImageTransitionSourceState(resource.layout, srcStageMask, srcAccessMask); - - VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - VkAccessFlags dstAccessMask = 0; - GetImageTransitionDestinationState(newLayout, dstStageMask, dstAccessMask); - - if (writtenMipLevel > 0) { - VkImageLayout lowerMipLayout = resource.layout; - const Bool lowerTransitioned = TransitionImageLayout( - commandBuffer, resource.image, lowerMipLayout, newLayout, - srcStageMask, dstStageMask, srcAccessMask, dstAccessMask, - resource.aspect, 0, writtenMipLevel); - MOBILEGL_ASSERT(lowerTransitioned, - "UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d", - texture->GetExternalIndex()); - } - - const Uint32 upperBaseMipLevel = writtenMipLevel + 1; - if (upperBaseMipLevel < resource.mipLevels) { - VkImageLayout upperMipLayout = resource.layout; - const Bool upperTransitioned = TransitionImageLayout( - commandBuffer, resource.image, upperMipLayout, newLayout, - srcStageMask, dstStageMask, srcAccessMask, dstAccessMask, - resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel); - MOBILEGL_ASSERT(upperTransitioned, - "UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d", - texture->GetExternalIndex()); - } - } - - resource.layout = newLayout; - } - Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, @@ -767,34 +628,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { } SizeT VkTextureManager::PruneDeadTextures() { - Vector expiredTextures; - expiredTextures.reserve(m_aliveObjects.size()); - for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) { - if (it->second.expired()) { - expiredTextures.emplace_back(it->first.texture); - } - } - for (auto* texture : expiredTextures) { - PruneStaleTextureAliases(texture); - } - SizeT prunedCount = expiredTextures.size(); - - // Orphan sweep: after the pass above, m_aliveObjects holds only live entries. - // Registration in SyncTextureAndGetDescriptor cannot fail for a SharedPtr-owned - // texture (weak_from_this fallback), so a resource whose identity has no alive - // entry has no trackable owner: its GL-side object is gone, or was never - // shared-owned, in which case recreation on a later sync is the safe fallback. - // Destruction goes through the per-frame deferred queues, never immediate. - Vector orphanIdentities; - for (auto it = m_textureResources.begin(); it != m_textureResources.end(); ++it) { - if (m_aliveObjects.find(it->first) == m_aliveObjects.end()) { - orphanIdentities.emplace_back(it->first); - } - } - for (const auto& identity : orphanIdentities) { - EraseTrackedTexture(identity); - } - prunedCount += orphanIdentities.size(); + // P13: only the handle-keyed tables are left; a resource whose record is no longer live + // at its generation is released through the per-frame deferred queues. + SizeT prunedCount = 0; const auto pruneWire = [&](auto& resources, const auto& records) { for (auto it = resources.begin(); it != resources.end();) { const Uint32 slot = static_cast(it->first >> 32) & 0x7fffffffu; @@ -2515,12 +2351,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_deferredViewReleases.clear(); } - MG_State::GLState::ITextureObject& VkTextureManager::StorageTextureOf( - MG_State::GLState::ITextureObject& texture) { - const auto& storageOwner = texture.GetViewStorageOwner(); - return storageOwner ? *storageOwner : texture; - } - VkImageAspectFlags VkTextureManager::GetAspectMaskForFormat(VkFormat format) { switch (format) { case VK_FORMAT_D16_UNORM: diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 00cff614..81c9ea0b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -108,27 +108,6 @@ inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAt return static_cast(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1)); } -// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture -// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they -// are relative to the VIEW, and have to be shifted into the storage image's numbering before they -// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the -// storage texture's (VkTextureManager::StorageTextureOf). -// -// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every -// GetOrCreate*View entry point below expects values that have already been through here, and so -// does everything that reads or copies an attachment directly. Both are identity on a plain -// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional -// and there is no second, view-only code path to keep in step. -inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) { - const Uint32 level = static_cast(glLevel > 0 ? glLevel : 0); - return texture != nullptr ? level + static_cast(texture->GetViewMinLevel()) : level; -} - -inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) { - const Uint32 layer = static_cast(glLayer > 0 ? glLayer : 0); - return texture != nullptr ? layer + static_cast(texture->GetViewMinLayer()) : layer; -} - class VkTextureManager { public: // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass @@ -139,23 +118,6 @@ public: // TextureResource pointers are valid only while this is unchanged. Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; } - struct TextureIdentity { - MG_State::GLState::ITextureObject* texture = nullptr; - Uint64 lifetimeId = 0; - - Bool operator==(const TextureIdentity& other) const { - return texture == other.texture && lifetimeId == other.lifetimeId; - } - }; - - struct TextureIdentityHash { - SizeT operator()(const TextureIdentity& key) const { - SizeT hash = std::hash{}(key.texture); - hash ^= std::hash{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2); - return hash; - } - }; - struct InitInfo { VkDevice device = VK_NULL_HANDLE; VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; @@ -536,17 +498,6 @@ public: // present-less frame-boundary drain. void CollectAllDeferredReleases(); - // ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ---- - // The GL texture whose STORAGE backs the given one: itself, or - for a texture created by - // glTextureView - the texture it views. Every image-scoped question (which VkImage, its - // LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view - // has none of its own; only the VkImageViews differ per GL texture object. Sharing one - // TextureResource is not an optimisation, it is the only correct arrangement: layout is a - // property of the image, and VulkanRenderer caches raw pointers straight to the resource's - // layout field, so a second resource aliasing the same image would desynchronise the moment - // either of them transitioned it. - static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture); - // The window a GL texture object opens onto its storage image. For a plain texture this is // the resource's own full extent; for a view it is the sub-range, format and aspect // glTextureView gave it. Views built from a non-default window must live in the KEYED caches @@ -575,8 +526,8 @@ public: // loudly, which is the honest answer a missing record deserves. // // The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle - - // a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen} - // can never inherit its predecessor's image. Texture views resolve the storage owner from + // keyed by the handle's {slot, gen}, so a recycled slot can never inherit its + // predecessor's image. Texture views resolve the storage owner from // Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm. Bool ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, Uint32 level, const IntVec3& extent, VkFormat& format, Vector& bytes); @@ -639,10 +590,6 @@ public: const auto it = m_wireTextureResources.find(key); return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr; } - void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, - MG_State::GLState::ITextureObject* texture, - Uint32 writtenMipLevel, - VkImageLayout newLayout); // Recording-generation bookkeeping for the pre-pass command stream. The // generation advances every time the frame command buffer (re)begins // recording; a resource whose stamp does not match was not referenced by @@ -652,8 +599,6 @@ public: void StampResourceRecordingUse(TextureResource& resource) const { resource.lastRecordingGeneration = m_recordingGeneration; } - // Map-lookup variant for callers that only hold the GL texture object. - void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture); Bool WasTouchedThisRecording(const TextureResource& resource) const { return resource.lastRecordingGeneration == m_recordingGeneration; } @@ -662,7 +607,6 @@ public: resource.lastRecordingGeneration = m_recordingGeneration; resource.lastRecordingWriteGeneration = m_recordingGeneration; } - void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture); // `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides // anything for an image that carries both aspects. Defaulted so the call sites that have no // texture in hand keep the depth-aspect answer they have always given. @@ -707,9 +651,6 @@ private: // the single queue, so the scan stops at the first still-pending entry). // waitAll blocks on every entry - Shutdown's drain. void ReclaimCompletedUploads(Bool waitAll = false); - static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture); - void EraseTrackedTexture(const TextureIdentity& identity); - void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture); SizeT PruneDeadTextures(); // The two halves of SyncTextureResourceByHandle: shape from the resource record's // descriptor (create / recreate / compatibility), then the pending-upload walk against the @@ -751,22 +692,6 @@ private: Uint32 m_gcFrameCounter = 0; // SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into. Bool m_syncingYuvTextureStorage = false; - // Cross-draw sampled-texture memo: the same few textures (atlas, lightmap) - // are resolved on every draw, so cache their resource pointers and skip the - // alive/resource map lookups. Node-based std::unordered_map keeps the - // pointees stable across inserts; erases bump m_resourceEraseEpoch, which - // every memo entry must match. SyncTexture still runs on memo hits, so - // content/param freshness is unaffected. A dead-then-reused texture address - // cannot false-hit: the new object carries a new lifetime id. - struct SyncedTextureMemoEntry { - const MG_State::GLState::ITextureObject* texture = nullptr; - Uint64 lifetimeId = 0; - Uint64 eraseEpoch = 0; - TextureResource* resource = nullptr; - }; - static constexpr Uint32 kSyncedTextureMemoSize = 8; - SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize]; - Uint32 m_syncedTextureMemoNext = 0; Uint64 m_resourceEraseEpoch = 1; // Formats whose mutable-image probe failed on this device; their images are created // without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch. @@ -775,23 +700,6 @@ private: // exactly like the mutable-format verdict above, so it is answered at image creation and // remembered rather than probed once globally. std::unordered_set m_2dArrayCompatibleUnsupported; - std::unordered_map, TextureIdentityHash> m_aliveObjects; - std::unordered_map m_textureResources; - // Textures that have been bound to a GL image unit (see MarkStorageImageTexture). - std::unordered_set m_storageImageTextures; - // Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by - // the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for - // the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers - // (it is what VK_KHR_image_format_list exists to claw back), so setting it on every - // immutable-storage texture would tax every glTexStorage2D render target in a game for a - // feature almost none of them use. A SAME-format view - which is the common case, and the - // Better Clouds case - needs no flag at all and therefore costs nothing. - std::unordered_map m_viewRequestedImageFlags; - // Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed - // VkImageFormatListCreateInfo the image is created with must name them: the list is a promise - // that NO other format will ever be viewed, and building a view outside it is - // VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture. - std::unordered_map, TextureIdentityHash> m_viewRequestedFormats; // Supported multisample counts per format, so repeat texture syncs do not // re-query vkGetPhysicalDeviceImageFormatProperties. std::unordered_map m_multisampleCountsByFormat; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index d5aa03e7..e23c09e5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2222,12 +2222,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_sampledReadStageMask, static_cast(m_physicalDevice.queueFamilies.graphicsFamily)}); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed."); - m_clearManager = MakeUnique(); - MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); - succeeded = m_clearManager->Initialize(); - MOBILEGL_ASSERT(succeeded, "VkClearManager initialization failed."); m_renderPassManager = - MakeUnique(m_device, m_physicalDevice.handle, m_allocator, m_config, *m_clearManager, + MakeUnique(m_device, m_physicalDevice.handle, m_allocator, m_config, *m_textureManager, m_swapchainObject); MOBILEGL_ASSERT(m_renderPassManager != nullptr, "VkRenderPassManager creation failed."); succeeded = m_renderPassManager->Initialize(); @@ -2290,7 +2286,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Aging evictions (render passes and program entries) must purge the dependent // pipeline / compute-pipeline / descriptor-set caches in the same step; both // sweeps only run from the frame-boundary seams, long after initialization. - m_renderPassManager->SetEvictionObserver(this); m_programFactory->SetEvictionObserver(this); m_samplerManager = MakeUnique(); @@ -2396,7 +2391,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // No sweep runs during teardown, but the observers point at this renderer // and the factories die at different times below; disconnect them first. if (m_renderPassManager) { - m_renderPassManager->SetEvictionObserver(nullptr); } if (m_programFactory) { m_programFactory->SetEvictionObserver(nullptr); @@ -2464,10 +2458,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_swapchainObject.Shutdown(m_device); } m_renderPassManager.reset(); - if (m_clearManager) { - m_clearManager->Shutdown(); - m_clearManager.reset(); - } if (m_commandPool != VK_NULL_HANDLE) { vkDestroyCommandPool(m_device, m_commandPool, nullptr); m_commandPool = VK_NULL_HANDLE; @@ -3995,22 +3985,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // and an overlap check). Refused outright, and refused for real rather than through an // assertion the release build drops: recording the pair anyway is a validation error and, // on a tiler, a copy whose source has already been overwritten. - // Compared by STORAGE, not by GL object: a texture view and the texture it views are two - // different objects over one VkImage (ARB_texture_view), and GL 4.6 core 8.18 explicitly - // permits copying between them - so an object-identity test would let exactly the case - // this guard exists for through. - const auto* srcStorageTexture = - srcEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*srcEndpoint.Texture) : nullptr; - const auto* dstStorageTexture = - dstEndpoint.Texture ? &VkTextureManager::StorageTextureOf(*dstEndpoint.Texture) : nullptr; - if ( - !wire && - srcStorageTexture == dstStorageTexture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) { - MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__, - CopyImageEndpointName(srcEndpoint)); - return; - } - // One resolver for both object kinds. The texture arm is the same // SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the // render-pass manager, which is where a renderbuffer's VkImage lives. @@ -4092,10 +4066,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { (void)mapSubresource(srcEndpoint, srcLevel, srcZ); dstStorageHandle = mapSubresource(dstEndpoint, dstLevel, dstZ); } - srcLevel = static_cast(ToStorageMipLevel(srcEndpoint.Texture.get(), srcLevel)); - dstLevel = static_cast(ToStorageMipLevel(dstEndpoint.Texture.get(), dstLevel)); - srcZ = static_cast(ToStorageArrayLayer(srcEndpoint.Texture.get(), srcZ)); - dstZ = static_cast(ToStorageArrayLayer(dstEndpoint.Texture.get(), dstZ)); + srcLevel = std::max(srcLevel, 0); + dstLevel = std::max(dstLevel, 0); + srcZ = std::max(srcZ, 0); + dstZ = std::max(dstZ, 0); if (srcLevel < 0 || dstLevel < 0 || static_cast(srcLevel) >= srcImage.mipLevels || static_cast(dstLevel) >= dstImage.mipLevels) { MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__, @@ -4242,7 +4216,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Bool dstClearReady = materializeClear(dstEndpoint); MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u", __func__, CopyImageEndpointName(dstEndpoint)); - m_textureManager->StampTextureRecordingWrite(dstEndpoint.Texture.get()); const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout; const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout; @@ -6059,9 +6032,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_textureManager->BeginFrame(frameIndex); } m_bufferManager.BeginFrame(frameIndex); - if (m_renderPassManager) { - m_renderPassManager->OnPresent(); - } // The pipeline memo can survive across these boundaries (no per-frame reset // on this path), so it must drop whenever the sweep destroys anything. if (m_programFactory) { @@ -6548,7 +6518,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { } MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); - m_renderPassManager->OnPresent(); // A real presented frame is the canonical aging cadence; mid-frame drains // count against this and only age when presents stop coming. m_drainsSinceLastPresent = 0; @@ -9134,7 +9103,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { DestroyWireDrawPass(); CollectWireObjects(m_submitCounter, true); ClearAllWireDrawPassCaches(); - if (m_renderPassManager) m_renderPassManager->PurgeRenderPasses(); InvalidatePipelineMemo(); ResetDynamicStateShadow(); DestroyParkedTarget(parked->second); @@ -9204,19 +9172,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_computePipelines.clear(); } - void VulkanRenderer::OnRenderPassesDestroyed(const Vector& renderPasses) { - if (m_pipelineFactory == nullptr) { - return; - } - // The render-pass sweep's >1024-boundary idle guarantee covers these pipelines - // too (they are only bound by draws that hit the dying entries), so the factory - // destroys them immediately. The memo must drop as well: it can hand out a - // cached handle without touching the factory. - if (m_pipelineFactory->EvictByRenderPasses(renderPasses) > 0) { - InvalidatePipelineMemo(); - } - } - void VulkanRenderer::OnProgramEvicted(ProgramFactory::HashType programHash, VkDescriptorSetLayout descriptorSetLayout) { // Same >1024-boundary idleness as the program entry: its compute pipeline is diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index e5fb470f..809ab604 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -132,7 +132,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver, - public VkRenderPassManager::IEvictionObserver, public ProgramFactory::IEvictionObserver { public: VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {}); @@ -226,12 +225,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { // recording, before any render pass. void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override; - // VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just - // destroyed these VkRenderPasses; evict every graphics pipeline hashed on a - // dying handle (they share its >1024-boundary idleness, so immediate - // destruction is safe) and drop the last-pipeline memo if any went. - void OnRenderPassesDestroyed(const Vector& renderPasses) override; - // ProgramFactory::IEvictionObserver: an aged-out program entry was // destroyed; evict its compute pipeline and graphics pipelines (same // idleness guarantee - they are only bound through draws/dispatches that @@ -1469,7 +1462,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { UniquePtr m_programFactory; UniquePtr m_uniformManager; UniquePtr m_vertexInputStateFactory; - UniquePtr m_clearManager; UniquePtr m_renderPassManager; UniquePtr m_textureManager; UniquePtr m_samplerManager; diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 924f962d..aa39b61f 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -26,7 +26,6 @@ namespace MobileGL::MG_State::GLState { namespace { - const BufferBackendOps* g_bufferBackendOps = nullptr; // Starts at 1 so a zero-initialized cache slot can never carry a live buffer's id. std::atomic g_nextBufferLifetimeId{1}; @@ -69,14 +68,6 @@ namespace MobileGL::MG_State::GLState { return g_nextBufferLifetimeId.fetch_add(1, std::memory_order_relaxed); } - void SetBufferBackendOps(const BufferBackendOps* ops) { - g_bufferBackendOps = ops; - } - - const BufferBackendOps* GetBufferBackendOps() { - return g_bufferBackendOps; - } - BufferObject::BufferObject(Uint externalIndex) : m_externalIndex(externalIndex), m_size(0), m_usage(BufferUsage::StaticDraw), m_isMapped(false), m_mappingAccess(BufferMappingAccessBit::Null), m_mappedRange({0, 0}), m_ownsStagingData{} { @@ -104,10 +95,7 @@ namespace MobileGL::MG_State::GLState { // while a backend's table was registered and destroyed after it was unregistered has // a pipe record to drop and no legacy backend object, and one constructed the other // way round has the opposite, so the create's answer is the only one that pairs. - if (MG_Pipe::MGPipeEmitResourceDestroyAndFree(*this)) return; - if (m_resource.Backend() && g_bufferBackendOps && g_bufferBackendOps->OnDestroy) { - g_bufferBackendOps->OnDestroy(m_resource.ReleaseBackend()); - } + (void)MG_Pipe::MGPipeEmitResourceDestroyAndFree(*this); } void BufferObject::NotifyRespecify() { @@ -115,10 +103,6 @@ namespace MobileGL::MG_State::GLState { MGP_NOTE_AGGREGATE(BufferChange); if (MG_Pipe::MGPipeResourceSubsystemEnabled()) { MG_Pipe::MGPipeEmitResourceRespecify(*this); - return; - } - if (g_bufferBackendOps && g_bufferBackendOps->Respecify) { - g_bufferBackendOps->Respecify(*this); } } @@ -129,10 +113,6 @@ namespace MobileGL::MG_State::GLState { m_hasDefinedContent = true; if (MG_Pipe::MGPipeResourceSubsystemEnabled()) { MG_Pipe::MGPipeEmitResourceSubData(*this, offset, size); - return; - } - if (g_bufferBackendOps && g_bufferBackendOps->SubData) { - g_bufferBackendOps->SubData(*this, offset, size); } } @@ -147,10 +127,6 @@ namespace MobileGL::MG_State::GLState { "real Flags, unnormalised"); MG_Pipe::MGPipeEmitResourceFlushRange(*this, range.start, range.end - range.start, static_cast(appAccess.GetRaw())); - return; - } - if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) { - g_bufferBackendOps->FlushMappedRange(*this, range, appAccess); } } @@ -279,19 +255,6 @@ namespace MobileGL::MG_State::GLState { if (m_isMapped) return; if (MG_Pipe::MGPipeResourceSubsystemEnabled()) { if (void* base = MG_Pipe::MGPipeEmitMapPersistent(*this)) m_resource.AdoptPersistentMap(base); - return; - } -#if MOBILEGL_BUILD_DISAGGREGATED - // R-6 IS "ALWAYS", AND THIS IS ITS SECOND DOOR. MGPipeApplyMapPersistent declines - // every acquisition under split - but a split build whose backend registered no - // MGPipe resource ops falls through to the LEGACY hook below, which would mint a real - // pointer and adopt it. A donated address is meaningless across a process, and an - // inproc lane that adopted would be green for a reason spawn cannot reproduce. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return; -#endif - if (g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) return; - if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) { - m_resource.AdoptPersistentMap(base); } } @@ -627,12 +590,7 @@ namespace MobileGL::MG_State::GLState { // the applier, so the caller sees the reconciled shadow on return exactly as it // does today. MG_Pipe::MGPipeEmitResourceReadback(*this); - return; } - if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) { - return; - } - g_bufferBackendOps->ReadbackFromGpu(*this); } void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) { @@ -699,15 +657,6 @@ namespace MobileGL::MG_State::GLState { NotifyContentWrite(offset, bytes.size); return; } - if (bytes.size > 0 && g_bufferBackendOps && g_bufferBackendOps->ResidentSubData) { - g_bufferBackendOps->ResidentSubData(*this, offset, bytes); - m_hasDefinedContent = true; - ++m_changeSerial; - MGP_NOTE_AGGREGATE(BufferChange); - m_gpuWritePending = true; - return; - } - SyncGpuWrites(); Memcpy(m_resource.Bytes() + offset, bytes.data, bytes.size); NotifyContentWrite(offset, bytes.size); @@ -734,9 +683,7 @@ namespace MobileGL::MG_State::GLState { // for nothing. if (m_resource.IsGpuResident() && // The same question, asked of whichever table owns the family in this build. - (MG_Pipe::MGPipeResourceSubsystemEnabled() - ? MG_Pipe::MGPipeResourceOpsHaveSubDataResident() - : (g_bufferBackendOps && g_bufferBackendOps->ResidentSubData)) + MG_Pipe::MGPipeResourceSubsystemEnabled() && MG_Pipe::MGPipeResourceOpsHaveSubDataResident() ) { Vector expanded(size); if (pattern.size == 1) { @@ -849,19 +796,7 @@ namespace MobileGL::MG_State::GLState { m_resource.AdoptPersistentMap(pushedBase); return true; } -#if MOBILEGL_BUILD_DISAGGREGATED - // R-6's second door, as in TryAdoptLargeStorage above. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return false; -#endif - if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) { - return false; - } - void* base = g_bufferBackendOps->AcquirePersistentMap(*this); - if (base == nullptr) { - return false; - } - m_resource.AdoptPersistentMap(base); - return true; + return false; } void* BufferObject::AcquireMemoryRange(Range1D range, Flags access) { @@ -923,18 +858,6 @@ namespace MobileGL::MG_State::GLState { } return m_resource.Bytes() + range.start; } - if (!m_resource.IsGpuResident() && (access & BufferMappingAccessBit::Write) && - !(access & BufferMappingAccessBit::FlushExplicit) && -#if MOBILEGL_BUILD_DISAGGREGATED - // R-6's second door, as in TryAdoptLargeStorage: no legacy mint under a - // transport, whatever the backend registered. - MG_Config::Transport == MG_Config::TransportMode::Monolith && -#endif - g_bufferBackendOps && g_bufferBackendOps->AcquirePersistentMap) { - if (void* base = g_bufferBackendOps->AcquirePersistentMap(*this)) { - m_resource.AdoptPersistentMap(base); - } - } return m_resource.Bytes() + range.start; } @@ -1007,14 +930,6 @@ namespace MobileGL::MG_State::GLState { return m_hasDefinedContent; } - const SharedPtr& BufferObject::GetBackendResource() const { - return m_resource.Backend(); - } - - void BufferObject::SetBackendResource(SharedPtr resource) { - m_resource.SetBackend(std::move(resource)); - } - Bool BufferObject::IsMapped() const { #if MOBILEGL_BUILD_DISAGGREGATED RefuseLegacyBufferArmFromApplyThread("IsMapped"); diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.h b/MobileGL/MG_State/GLState/BufferState/BufferObject.h index 5530e082..0e71e48b 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.h @@ -64,68 +64,6 @@ namespace MobileGL { // either the CPU shadow or the backend's persistently-mapped GPU memory) live // in PipeResource.h. - // Immediate buffer transfer interface implemented by the active backend - // (the pipe_context buffer-op analogue). Ops are invoked at GL call time, - // right after the shadow copy has been updated; contents are always read - // from the shadow so ops carry only ranges and flags. - // - // Every op must tolerate bufferObject.GetBackendResource() == nullptr: - // resources are created lazily by the backend's draw/bind-time ensure - // path, which performs a full upload from the shadow and thereby covers - // all ops that happened before the resource existed. - struct BufferBackendOps { - // Storage (re)definition: glBufferData / glBufferStorage. The orphaning - // point - the backend decides (busy-tracking) whether to swap storage - // or write in place. Shadow already holds the new contents. - void (*Respecify)(BufferObject& bufferObject) = nullptr; - // Contents update of [offset, offset + size) from the shadow. - void (*SubData)(BufferObject& bufferObject, SizeT offset, SizeT size) = nullptr; - // Contents update of an ADOPTED (GPU-resident) store. `data` holds the app's - // bytes, valid for the duration of the call only (a write map's staging - // store is freed the moment the unmap that lands it returns); the frontend - // has NOT touched the resident mapping. GL orders a - // glBufferSubData after already-submitted GPU reads of the store, and an - // in-place host write into the coherent mapping tears the frames still - // reading the old bytes (Minecraft patches LIVE chunk sections this way - - // the tear shows as one-frame wrong geometry/UVs during fast movement). The - // backend lands the bytes on the GPU timeline instead: after in-flight - // readers, before the next consumer. The frontend marks the buffer - // gpu-write-pending so reads reconcile through ReadbackFromGpu. Backends - // without this op keep the legacy ordered in-place host write. - void (*ResidentSubData)(BufferObject& bufferObject, SizeT offset, DataPtr data) = nullptr; - // Write-map flush (glUnmapBuffer / glFlushMappedBufferRange). Carries the - // app's real mapping flags so the backend can honour INVALIDATE_* / - // UNSYNCHRONIZED semantics per call instead of merging them. - void (*FlushMappedRange)(BufferObject& bufferObject, Range1D range, - Flags appAccess) = nullptr; - // Final release of the backend resource (called from ~BufferObject). - // The backend defers actual destruction until the GPU is done with it. - void (*OnDestroy)(SharedPtr&& resource) = nullptr; - // Zero-copy persistent mapping. For a coherent (non-FLUSH_EXPLICIT) persistent - // write map, the backend may hand back a host-visible, COHERENT, persistently - // mapped pointer into its own GPU storage for the whole buffer [0, size), - // created with every buffer usage and seeded from the shadow. From that point - // the GPU buffer is the single source of truth: the app writes into it - // directly, all reads/writes resolve against it (HostData()), and NO further - // backend transfer ops are dispatched for this buffer. Returns nullptr when the - // backend cannot back the map; the frontend then keeps the CPU-shadow model. - // Must be idempotent: a second call for an already-backed buffer returns the - // same base pointer. - void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr; - // Pulls the backend's current contents for the whole buffer into the shadow - // (through WritebackFromBackend). Only ever called for a buffer the GPU may - // have written behind the frontend's back - a shader storage or atomic counter - // binding of a draw or dispatch - because nothing else can desynchronise the - // shadow. Backends that cannot read their storage back leave this null; the - // shadow then keeps its pre-dispatch bytes, which is the old behaviour. - void (*ReadbackFromGpu)(BufferObject& bufferObject) = nullptr; - }; - - // Registered by the active backend at init, cleared at shutdown. - // Null table (unit tests, benchmarks) => shadow-only state tracking. - void SetBufferBackendOps(const BufferBackendOps* ops); - const BufferBackendOps* GetBufferBackendOps(); - class BufferObject { public: using TargetEnum = BufferTarget; @@ -273,9 +211,6 @@ namespace MobileGL { // uploading the stale CPU shadow. Bool HasDefinedContent() const; - const SharedPtr& GetBackendResource() const; - void SetBackendResource(SharedPtr resource); - private: // Sizes the store for a (re)definition, renewing an adopted GPU-resident // mapping across it. See the definition for why the renewal is not optional. diff --git a/MobileGL/MG_State/GLState/BufferState/PipeResource.h b/MobileGL/MG_State/GLState/BufferState/PipeResource.h index 9ab1c11b..81368781 100644 --- a/MobileGL/MG_State/GLState/BufferState/PipeResource.h +++ b/MobileGL/MG_State/GLState/BufferState/PipeResource.h @@ -93,7 +93,8 @@ namespace MobileGL::MG_State::GLState { // Opaque, refcounted handle to the backend's GPU storage for one buffer // (the driver-side resource). The active backend derives from it and attaches - // its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource. + // its own payload (GLESBufferResource); since P13 it is held by the backend's own + // handle-keyed table, never by the frontend object. class BackendBufferResource { public: virtual ~BackendBufferResource() = default; @@ -184,18 +185,12 @@ namespace MobileGL::MG_State::GLState { // errors the frontend refuses before reaching here. void ReleasePersistentMap() { m_gpuMapped = nullptr; } - // Backend GPU resource, owned here in both modes. - const SharedPtr& Backend() const { return m_backend; } - void SetBackend(SharedPtr backend) { m_backend = std::move(backend); } - SharedPtr ReleaseBackend() { return std::move(m_backend); } - private: // MapAlignedData, not Data: a read-only glMapBuffer hands the application this very // pointer, and a range map hands it base + offset, so the base has to be on the // GL_MIN_MAP_BUFFER_ALIGNMENT grid for either to satisfy ARB_map_buffer_alignment. SharedPtr m_shadow = MakeShared(); void* m_gpuMapped = nullptr; - SharedPtr m_backend; // See ShadowAllocationBytes. Split-only so the pull build's layout does not move (G1). SizeT m_shadowExtent = 0; }; diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 3fb5e357..a4fe3e2b 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -1556,7 +1556,7 @@ TEST_F(GeneralBufferTest, General_GeneralTest_1) { // --------------------------------------------------------------------------- // Zero-copy persistent-coherent mapping via the PipeResource layer (regression // guard for the GpuMemory OOM / rendering-corruption bug). A fake backend hands -// out a block of "GPU" memory from AcquirePersistentMap; the frontend adopts it +// out a block of "GPU" memory from MapPersistent; the frontend adopts it // as the buffer's storage. The test asserts (a) the app maps straight onto that // GPU memory, (b) EVERY reader (MappedData(), the accessor all backend consumers // now use) resolves to that same GPU memory rather than a stale shadow - the bug @@ -1583,50 +1583,53 @@ namespace { ZeroCopyMockBackend* g_zeroCopyMock = nullptr; - void* ZeroCopyMock_AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject) { + // P13: the mock is a MGPipeResourceOps table - the live resource family every backend + // registers - and no longer the deleted BufferBackendOps table. The frontend reaches it the + // way it reaches a real backend in monolith: the emitters build the records and the applier + // dispatches them synchronously. + void* ZeroCopyMock_MapPersistent(MG_Pipe::MGPipeHandle, Uint64 size, const void* seedBytes) { if (!g_zeroCopyMock || !g_zeroCopyMock->provideMap) return nullptr; - if (g_zeroCopyMock->gpu.size() != bufferObject.GetSize()) { - g_zeroCopyMock->gpu.assign(bufferObject.GetSize(), 0); - // Seed from the shadow (still current: the frontend adopts only after we return). - const Uint8* shadow = bufferObject.MappedData(); - if (shadow != nullptr && bufferObject.GetSize() > 0) { - Memcpy(g_zeroCopyMock->gpu.data(), shadow, bufferObject.GetSize()); + if (g_zeroCopyMock->gpu.size() != size) { + g_zeroCopyMock->gpu.assign(static_cast(size), 0); + // Seed from the shadow the frontend hands over (it adopts only after we return). + if (seedBytes != nullptr && size > 0) { + Memcpy(g_zeroCopyMock->gpu.data(), seedBytes, static_cast(size)); } } ++g_zeroCopyMock->acquireMapCalls; return g_zeroCopyMock->gpu.data(); } - void ZeroCopyMock_Respecify(MG_State::GLState::BufferObject&) { + void ZeroCopyMock_Respecify(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPResourceDesc&, const void*) { if (g_zeroCopyMock) ++g_zeroCopyMock->respecifyCalls; } - void ZeroCopyMock_SubData(MG_State::GLState::BufferObject&, SizeT, SizeT) { + void ZeroCopyMock_SubData(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPSubData&, const void*) { if (g_zeroCopyMock) ++g_zeroCopyMock->subDataCalls; } - void ZeroCopyMock_Flush(MG_State::GLState::BufferObject&, Range1D, Flags) { + void ZeroCopyMock_Flush(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPFlushRange&, const void*) { if (g_zeroCopyMock) ++g_zeroCopyMock->flushCalls; } - void ZeroCopyMock_OnDestroy(SharedPtr&&) {} + void ZeroCopyMock_Destroy(MG_Pipe::MGPipeHandle) {} - const MG_State::GLState::BufferBackendOps kZeroCopyMockOps = { + const MG_Pipe::MGPipeResourceOps kZeroCopyMockOps = { .Respecify = ZeroCopyMock_Respecify, .SubData = ZeroCopyMock_SubData, - .FlushMappedRange = ZeroCopyMock_Flush, - .OnDestroy = ZeroCopyMock_OnDestroy, - .AcquirePersistentMap = ZeroCopyMock_AcquirePersistentMap, + .FlushRange = ZeroCopyMock_Flush, + .Destroy = ZeroCopyMock_Destroy, + .MapPersistent = ZeroCopyMock_MapPersistent, }; // The same backend with the GPU-ordered landing ops a staging-ring backend offers: a // CPU write into an adopted store is queued (the mapping is NOT written through), and // a readback is what lands the queue before the application reads. - void ZeroCopyMock_ResidentSubData(MG_State::GLState::BufferObject&, SizeT offset, DataPtr data) { + void ZeroCopyMock_ResidentSubData(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPSubData& record, const void* bytes) { if (!g_zeroCopyMock) return; auto& write = g_zeroCopyMock->residentWrites.emplace_back(); - write.offset = offset; - const auto* bytes = static_cast(data.data); - write.bytes.assign(bytes, bytes + data.size); + write.offset = static_cast(MG_Pipe::MGPipeSubDataBufferOffset(record)); + const auto* first = static_cast(bytes); + write.bytes.assign(first, first + static_cast(MG_Pipe::MGPipeSubDataBufferSize(record))); } - void ZeroCopyMock_ReadbackFromGpu(MG_State::GLState::BufferObject&) { + void ZeroCopyMock_Readback(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPReadback&) { if (!g_zeroCopyMock) return; ++g_zeroCopyMock->readbackCalls; for (const auto& write : g_zeroCopyMock->residentWrites) { @@ -1639,32 +1642,32 @@ namespace { g_zeroCopyMock->residentWrites.clear(); } - const MG_State::GLState::BufferBackendOps kResidentSubDataMockOps = { + const MG_Pipe::MGPipeResourceOps kResidentSubDataMockOps = { .Respecify = ZeroCopyMock_Respecify, .SubData = ZeroCopyMock_SubData, - .ResidentSubData = ZeroCopyMock_ResidentSubData, - .FlushMappedRange = ZeroCopyMock_Flush, - .OnDestroy = ZeroCopyMock_OnDestroy, - .AcquirePersistentMap = ZeroCopyMock_AcquirePersistentMap, - .ReadbackFromGpu = ZeroCopyMock_ReadbackFromGpu, + .SubDataResident = ZeroCopyMock_ResidentSubData, + .FlushRange = ZeroCopyMock_Flush, + .Readback = ZeroCopyMock_Readback, + .Destroy = ZeroCopyMock_Destroy, + .MapPersistent = ZeroCopyMock_MapPersistent, }; - // These cases are BufferBackendOps dispatch tests. Since P3a a push build also installs the - // handle-shaped MGPipeResourceOps table at backend bring-up, and the frontend's push arms route - // every buffer mutation there first; the fixture therefore scopes BOTH tables, the way - // ResourceEmitTest's ApplierGuard scopes the applier, so the mock installed here is the arm - // that actually runs. The pipe-side dispatch has its own coverage in ResourceEmitTest. + // Installs the mock as the process's resource op table for the case and arms the resource + // subsystem bit (a fixture that never ran Initialize() has not loaded the fixed mask), and + // puts both back afterwards. struct ScopedBackendOps { - explicit ScopedBackendOps(const MG_State::GLState::BufferBackendOps* ops) { - MG_State::GLState::SetBufferBackendOps(ops); + explicit ScopedBackendOps(const MG_Pipe::MGPipeResourceOps* ops) { + m_savedPush = MG_Config::Features.PipePush; + MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemResources; m_savedResourceOps = MG_Pipe::MGPipeGetResourceOps(); - MG_Pipe::MGPipeSetResourceOps(nullptr); + MG_Pipe::MGPipeSetResourceOps(ops); } ~ScopedBackendOps() { MG_Pipe::MGPipeSetResourceOps(m_savedResourceOps); - MG_State::GLState::SetBufferBackendOps(nullptr); + MG_Config::Features.PipePush = m_savedPush; } const MG_Pipe::MGPipeResourceOps* m_savedResourceOps = nullptr; + Uint64 m_savedPush = 0; }; } // namespace diff --git a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp index 89e10a88..15a7a6d0 100644 --- a/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/SplitBufferTest.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -42,20 +43,38 @@ namespace { using MG_Record::PersistentMapTracker; // A backend that MINTS a persistent mapping, for the one case that needs the adopted arm - // (TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo). Only AcquirePersistentMap is - // filled in: the frontend null-checks every op individually, and a table with one live - // member is the smallest thing that makes AcquireMemoryRange's legacy adoption arm fire. - // The storage is the CASE's, not this table's - AdoptPersistentMap keeps the pointer and - // never owns it - so the base travels through a file-scope variable the case sets and - // clears around the one acquisition it wants minted. + // (TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo): a resource op table (P13: the + // only buffer op table there is) whose one live member is MapPersistent. The storage is the + // CASE's, not this table's - AdoptPersistentMap keeps the pointer and never owns it - so the + // base travels through a file-scope variable the case sets and clears. void* g_mintedPersistentBase = nullptr; - void* MintPersistentMap(BufferObject&) { return g_mintedPersistentBase; } - const MG_State::GLState::BufferBackendOps g_mintingBufferOps = [] { - MG_State::GLState::BufferBackendOps ops{}; - ops.AcquirePersistentMap = &MintPersistentMap; + void* MintPersistentMap(MG_Pipe::MGPipeHandle, Uint64, const void*) { return g_mintedPersistentBase; } + const MG_Pipe::MGPipeResourceOps g_mintingResourceOps = [] { + MG_Pipe::MGPipeResourceOps ops{}; + ops.MapPersistent = &MintPersistentMap; return ops; }(); + // The window in which the case is a MONOLITH with that backend registered: the adopted buffer + // is created, mapped and destroyed inside one, so its records are applied synchronously. + struct ScopedMintingMonolith { + ScopedMintingMonolith() + : m_transport(MG_Config::Transport), m_push(MG_Config::Features.PipePush), + m_ops(MG_Pipe::MGPipeGetResourceOps()) { + MG_Config::Transport = MG_Config::TransportMode::Monolith; + MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemResources; + MG_Pipe::MGPipeSetResourceOps(&g_mintingResourceOps); + } + ~ScopedMintingMonolith() { + MG_Pipe::MGPipeSetResourceOps(m_ops); + MG_Config::Features.PipePush = m_push; + MG_Config::Transport = m_transport; + } + MG_Config::TransportMode m_transport; + Uint64 m_push; + const MG_Pipe::MGPipeResourceOps* m_ops; + }; + // Everything in this package is gated on `Transport != Monolith`, so every case has to // put the process into a split configuration and put it back. A fixture rather than a // lambda because the tracker is a process-wide singleton and a case that left an entry in @@ -82,7 +101,6 @@ namespace { // Belt and braces for the one case that installs a minting backend: a table left // behind would make the NEXT case's acquisition land in the adopted arm, and every // membership assertion in this file would then be about a different code path. - MG_State::GLState::SetBufferBackendOps(nullptr); g_mintedPersistentBase = nullptr; MG_Remote::Transport::ForgetAdoptTierSettlementForTest(); PersistentMapTracker::Instance().ClearForTest(); @@ -327,7 +345,11 @@ TEST_F(SplitBufferSet, TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo) { Vector minted(kSize, static_cast(0)); g_mintedPersistentBase = minted.data(); - auto adopted = MakeBuffer(27u, kSize); + SharedPtr adopted; + { + ScopedMintingMonolith minting; + adopted = MakeBuffer(27u, kSize); + } auto declined = MakeBuffer(28u, kSize); // The declined twin first, with no backend ops at all: same flags, same size, same call. @@ -339,12 +361,9 @@ TEST_F(SplitBufferSet, TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo) { "assertion below proves nothing"; { - MG_State::GLState::SetBufferBackendOps(&g_mintingBufferOps); - MG_Config::Transport = MG_Config::TransportMode::Monolith; + ScopedMintingMonolith minting; adopted->AcquireMemoryRange(Range1D{0, kSize}, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent); - MG_Config::Transport = MG_Config::TransportMode::InProcess; - MG_State::GLState::SetBufferBackendOps(nullptr); } ASSERT_TRUE(adopted->IsBackendPersistentMapped()) << "the backend declined the mint, so there is no adopted arm here to ask about"; @@ -383,7 +402,10 @@ TEST_F(SplitBufferSet, TheAdoptedArmIsNotAMemberAndItIsTheChainRowThatSaysSo) { // being redefined, and a persistent map the application holds outlives every unmap by // definition (PipeResource.h:121-127). It is dropped here with the mapping still adopted, // which is also the shape ~BufferObject has to survive. - adopted.reset(); + { + ScopedMintingMonolith minting; + adopted.reset(); + } g_mintedPersistentBase = nullptr; } diff --git a/scripts/data/link_ratchet_baseline.txt b/scripts/data/link_ratchet_baseline.txt index f0e5c500..f038eeed 100644 --- a/scripts/data/link_ratchet_baseline.txt +++ b/scripts/data/link_ratchet_baseline.txt @@ -5,7 +5,7 @@ # base commit : # configure : -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DCMAKE_BUILD_TYPE=Release # build dir : /home/swung/mgl-p13-split -# count : 36 +# count : 27 # # One demangled symbol per line. A trailing ` # note` is an annotation the parser # ignores. `# P13` (debts only P13's module work could clear) is refused since @@ -25,25 +25,16 @@ MobileGL::MG_Remote::Client::ClientSession::StartSpawned() MobileGL::MG_Remote::Client::ClientSession::Stop() MobileGL::MG_Remote::Client::ClientSessionInstance() MobileGL::MG_Remote::Client::EmitObjectDeathRecord(MobileGL::MG_Pipe::MGPipeKind, unsigned long) -MobileGL::MG_State::GLState::BufferObject::GetBackendResource() const -MobileGL::MG_State::GLState::BufferObject::GetChangeSerial() const MobileGL::MG_State::GLState::BufferObject::GetExternalIndex() const MobileGL::MG_State::GLState::BufferObject::GetSize() const -MobileGL::MG_State::GLState::BufferObject::GetUsage() const -MobileGL::MG_State::GLState::BufferObject::HasDefinedContent() const -MobileGL::MG_State::GLState::BufferObject::MappedData() const -MobileGL::MG_State::GLState::BufferObject::SetBackendResource(std::shared_ptr) -MobileGL::MG_State::GLState::BufferObject::SyncPersistentMappedRange() MobileGL::MG_State::GLState::BufferObject::WritebackFromBackend(MobileGL::DataPtr, unsigned long) MobileGL::MG_State::GLState::ClassifyVertexAttribType(unsigned int) MobileGL::MG_State::GLState::DecodeProgramArchive(unsigned char const*, unsigned long, MobileGL::MG_State::GLState::ProgramArchive&) -MobileGL::MG_State::GLState::GetBufferBackendOps() MobileGL::MG_State::GLState::ProgramArtifactsSchemaFingerprint() MobileGL::MG_State::GLState::ProgramObject::JoinPendingLink() const MobileGL::MG_State::GLState::SamplerObject::GetAllSamplerParameters() const MobileGL::MG_State::GLState::SamplerObject::GetExternalIndex() const MobileGL::MG_State::GLState::SamplerObject::GetLifetimeId() const -MobileGL::MG_State::GLState::SetBufferBackendOps(MobileGL::MG_State::GLState::BufferBackendOps const*) MobileGL::MG_State::GLState::TextureObjectBuffer::GetBufferBindingSlot(MobileGL::TextureUploadTarget) MobileGL::MG_State::GLState::TextureUnit::GetAllBindingSlots() MobileGL::MG_State::GLState::TextureUnit::GetSamplerObject() const