diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 55d5d174..6be8413b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -15,6 +15,12 @@ #include "MG_Pipe/MGPipeCallbacks.h" #include "MG_Pipe/PipeApply.h" #include "MG_Remote/Server/StagedShadow.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include "MG_Remote/Transport/AdoptT0.h" +#endif +#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__) +#include +#endif #include #include #endif @@ -206,6 +212,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { // modified ranges. A server pointer is never donated across the wire. .MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; }, .UnmapPersistent = [](MG_Pipe::MGPipeHandle) {}, +#if defined(__ANDROID__) + // P11 B2 (T0): the one door a server pointer is still never donated through stays shut; + // what T0 adds is the other direction - the CLIENT's pages imported as the store. + .ImportExternal = [](MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) -> Bool { + return WireManager().ImportWireBuffer(res, ahb, size); + }, + .SelfTestExternal = [](char* why, Uint64 whyBytes) -> Bool { + return VkBufferManager::SelfTestWireImport(why, whyBytes); + }, +#endif }; #endif } // namespace @@ -245,7 +261,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // to wait for a submission (DeferredWireRelease), and three lines from now the record // no longer carries it. const Uint64 orphanedUseSerial = resource->lastUseSerial; - DeferWireRelease(std::move(resource->buffer), orphanedUseSerial); + // P11 B2: a T0 store is ALWAYS released through the serial-gated list (never "at once"), + // so the client's AHardwareBuffer outlives every submission that can still name it however + // the client's own release (at this respecify) raced the frames in flight. + DeferWireRelease(std::move(resource->buffer), + resource->imported ? T0ReleaseSerial(orphanedUseSerial) : orphanedUseSerial); + resource->imported = false; resource->size = desc.Width; resource->lastUseSerial = 0; resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store) @@ -537,11 +558,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) { - Vector bytes(static_cast(size)); - if (!ReadWireBuffer(res, offset, size, bytes.data())) { + // P11 B2: A T0 STORE'S READBACK IS "DONE, NO BYTES". Its pages are the client's + // AHardwareBuffer, so what the client needs before it reads them is this server's GPU + // having finished every write to them - the resident copies WriteWireBuffer recorded and + // any shader write - and nothing crossing SEG_EVENT. The wait is the store's own host-access + // wait (a barrier to host, then the sync point's submission); the reply then says "done". + // One death for both shapes: the store could not be made readable. + auto* resource = FindWireBuffer(res); + const Bool t0 = resource != nullptr && resource->imported; + Vector bytes(t0 ? 0 : static_cast(size)); + if (t0 ? !WaitForWireBufferHostAccess(*resource) : !ReadWireBuffer(res, offset, size, bytes.data())) { MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen); std::abort(); } + if (t0) { + ++m_sliceEpochCounter; + return; + } if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) { MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}"); std::abort(); @@ -573,10 +606,305 @@ namespace MobileGL::MG_Backend::DirectVulkan { MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range); } + // ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A WIRE STORE ----------------------------- + + // The serial a T0 store is released under: its last use, or - when no GPU command has named it + // since a host-access wait - the current frame's, so the release still waits for the sync + // point rather than taking DeferWireRelease's "never named, destroy now" arm. Conservative by + // at most one submission. + Uint64 VkBufferManager::T0ReleaseSerial(Uint64 lastUseSerial) const { + if (lastUseSerial != 0) return lastUseSerial; + return m_frameSerial != 0 ? m_frameSerial : 1; + } + +#if defined(__ANDROID__) + namespace { + Int32 T0PickMemoryType(const VkPhysicalDeviceMemoryProperties& props, Uint32 bits, + VkMemoryPropertyFlags want) { + for (Uint32 i = 0; i < props.memoryTypeCount; ++i) { + if ((bits & (1u << i)) != 0 && (props.memoryTypes[i].propertyFlags & want) == want) + return static_cast(i); + } + return -1; + } + + struct T0ImportedBuffer { + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + void* mapped = nullptr; + Bool coherent = true; + }; + + void T0DestroyImported(VkDevice device, T0ImportedBuffer& imported) { + if (imported.mapped != nullptr) vkUnmapMemory(device, imported.memory); + if (imported.buffer != VK_NULL_HANDLE) vkDestroyBuffer(device, imported.buffer, nullptr); + if (imported.memory != VK_NULL_HANDLE) vkFreeMemory(device, imported.memory, nullptr); + imported = T0ImportedBuffer{}; + } + + // A VkBuffer on a dedicated allocation IMPORTED from `ahb`, in a host-visible memory type + // (coherent preferred), mapped whole. The server's own host map is what WriteWireBuffer's + // idle path and every CPU reader of a wire store use, so a type without HOST_VISIBLE is a + // refusal, not a degraded import. Nothing is left behind on failure. + Bool T0ImportAhb(const VulkanRenderer::WireAhbImport& ctx, void* ahb, Uint64 size, VkBufferUsageFlags usage, + T0ImportedBuffer& out, String& why) { + out = T0ImportedBuffer{}; + const auto getProperties = + reinterpret_cast(ctx.getAhbProperties); + VkAndroidHardwareBufferPropertiesANDROID properties{ + VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID}; + VkResult result = getProperties(ctx.device, static_cast(ahb), &properties); + if (result != VK_SUCCESS) { + why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast(result)); + return false; + } + VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; + external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID; + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.pNext = &external; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + result = vkCreateBuffer(ctx.device, &bufferInfo, nullptr, &out.buffer); + if (result != VK_SUCCESS) { + out.buffer = VK_NULL_HANDLE; + why = "vkCreateBuffer(external AHB) -> " + std::to_string(static_cast(result)); + return false; + } + Int32 type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + out.coherent = type >= 0; + if (type < 0) + type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + if (type < 0) { + why = "no host-visible memory type can import it (memoryTypeBits 0x" + + std::to_string(properties.memoryTypeBits) + ")"; + T0DestroyImported(ctx.device, out); + return false; + } + VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO}; + dedicated.buffer = out.buffer; + VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID}; + import.buffer = static_cast(ahb); + import.pNext = &dedicated; + VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocateInfo.pNext = &import; + allocateInfo.allocationSize = properties.allocationSize; + allocateInfo.memoryTypeIndex = static_cast(type); + result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory); + if (result != VK_SUCCESS) { + out.memory = VK_NULL_HANDLE; + why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast(result)); + T0DestroyImported(ctx.device, out); + return false; + } + result = vkBindBufferMemory(ctx.device, out.buffer, out.memory, 0); + if (result == VK_SUCCESS) result = vkMapMemory(ctx.device, out.memory, 0, VK_WHOLE_SIZE, 0, &out.mapped); + if (result != VK_SUCCESS || out.mapped == nullptr) { + out.mapped = nullptr; + why = "vkBindBufferMemory/vkMapMemory -> " + std::to_string(static_cast(result)); + T0DestroyImported(ctx.device, out); + return false; + } + return true; + } + } // namespace +#endif + + Bool VkBufferManager::ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) { +#if defined(__ANDROID__) + auto* resource = FindWireBuffer(res); + if (resource == nullptr || ahb == nullptr || size == 0 || size != resource->size) return false; + VulkanRenderer::WireAhbImport ctx; + if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) { + MGLOG_W_ONCE("Magma T0: this device did not take VK_ANDROID_external_memory_android_hardware_buffer"); + return false; + } + VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT; + T0ImportedBuffer imported; + String why; + if (!T0ImportAhb(ctx, ahb, size, usage, imported, why)) { + MGLOG_W("Magma T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen, + static_cast(size), why.c_str()); + return false; + } + // The T2 store it replaces goes the way a respecify's does (and any draw still naming it + // keeps it until its submission completes). + DeferWireRelease(std::move(resource->buffer), resource->lastUseSerial); + MG_Remote::Transport::AdoptT0::AcquireImported(ahb); + resource->buffer.AdoptExternal(ctx.device, imported.buffer, imported.memory, size, imported.mapped, + imported.coherent, ahb); + resource->imported = true; + resource->lastUseSerial = 0; + resource->lastUseSubmitIndex = 0; + resource->gpuWritesPending = false; + // The whole store holds bytes the client supplied (its AHB, seeded from its shadow). + resource->stagedCoverage.clear(); + MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast(size)); + ++m_sliceEpochCounter; + ++m_wireStoreCount; + NoteWireStorePeaks(); + PublishWireReclaimGauges(); + MGLOG_D("Magma T0: imported {slot=%u, gen=%u} (%llu bytes, %s memory)", res.Slot, res.Gen, + static_cast(size), imported.coherent ? "coherent" : "non-coherent"); + return true; +#else + (void)res; + (void)ahb; + (void)size; + return false; +#endif + } + + // THE POST (B2; decided by probe, never by driver name). The AHB API promises CPU coherence only + // across an unlock/lock, and T0 holds the client's lock for the store's life, so this exercises + // that pattern on THIS device before kCapAdoptT0 may be published: + // hold - a 64 KiB BLOB allocated and locked by the test, never unlocked while it runs; + // read - this server's own host map of the import reads what the CPU wrote through the lock; + // gpu - one submission copies that pattern out of the import (the GPU read) and fills another + // region of it (the GPU write), then a transfer->host barrier and a fence wait; + // back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer. + Bool VkBufferManager::SelfTestWireImport(char* whyOut, Uint64 whyBytes) { + const auto answer = [&](Bool ok, const String& text) { + std::snprintf(whyOut, static_cast(whyBytes), "%s", text.c_str()); + return ok; + }; +#if defined(__ANDROID__) + VulkanRenderer::WireAhbImport ctx; + if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) + return answer(false, "the device did not take VK_ANDROID_external_memory_android_hardware_buffer"); + constexpr Uint64 kBytes = 64 * 1024; + constexpr Uint32 kWords = 1024; // one 4 KiB region + constexpr Uint64 kFillAt = 4 * 4096; // region 4 + constexpr Uint32 kFillWord = 0x6C0FFEE0u; + Vector pattern(kBytes / 4); + for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast(i * 2654435761u); + MG_Remote::Transport::AdoptT0::HeldStore held; + String why; + if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why)) + return answer(false, "hold: " + why); + T0ImportedBuffer imported, staging; + VkCommandPool pool = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + String verdict; + Bool ok = false; + do { + if (!T0ImportAhb(ctx, held.ahb, kBytes, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + imported, why)) { + verdict = "import: " + why; + break; + } + if (!imported.coherent) { + VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; + range.memory = imported.memory; + range.size = VK_WHOLE_SIZE; + vkInvalidateMappedMemoryRanges(ctx.device, 1, &range); + } + if (std::memcmp(imported.mapped, pattern.data(), kWords * 4) != 0) { + verdict = "read: the server's host map of the import does not hold the CPU's pattern"; + break; + } + // A host-visible staging buffer for the GPU read's destination. + VkBufferCreateInfo stagingInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + stagingInfo.size = kWords * 4; + stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(ctx.device, &stagingInfo, nullptr, &staging.buffer) != VK_SUCCESS) { + staging.buffer = VK_NULL_HANDLE; + verdict = "staging buffer"; + break; + } + VkMemoryRequirements requirements{}; + vkGetBufferMemoryRequirements(ctx.device, staging.buffer, &requirements); + const Int32 stagingType = T0PickMemoryType(ctx.memory, requirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + VkMemoryAllocateInfo stagingAlloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + stagingAlloc.allocationSize = requirements.size; + stagingAlloc.memoryTypeIndex = static_cast(stagingType < 0 ? 0 : stagingType); + if (stagingType < 0 || vkAllocateMemory(ctx.device, &stagingAlloc, nullptr, &staging.memory) != VK_SUCCESS || + vkBindBufferMemory(ctx.device, staging.buffer, staging.memory, 0) != VK_SUCCESS || + vkMapMemory(ctx.device, staging.memory, 0, VK_WHOLE_SIZE, 0, &staging.mapped) != VK_SUCCESS) { + verdict = "staging memory"; + break; + } + std::memset(staging.mapped, 0, kWords * 4); + VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + poolInfo.queueFamilyIndex = ctx.queueFamily; + poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + VkCommandBuffer commands = VK_NULL_HANDLE; + VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandInfo.commandBufferCount = 1; + VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + if (vkCreateCommandPool(ctx.device, &poolInfo, nullptr, &pool) != VK_SUCCESS) { + pool = VK_NULL_HANDLE; + verdict = "command pool"; + break; + } + commandInfo.commandPool = pool; + if (vkAllocateCommandBuffers(ctx.device, &commandInfo, &commands) != VK_SUCCESS || + vkCreateFence(ctx.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) { + verdict = "command buffer / fence"; + break; + } + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(commands, &begin); + VkMemoryBarrier hostWrites{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + hostWrites.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + hostWrites.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, + &hostWrites, 0, nullptr, 0, nullptr); + VkBufferCopy region{0, 0, kWords * 4}; + vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, ®ion); + vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kFillWord); + VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, + 0, nullptr, 0, nullptr); + vkEndCommandBuffer(commands); + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &commands; + if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS || + vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) { + verdict = "gpu: the submission did not complete within 2 s"; + break; + } + Uint32 gpuRead = 0, heldSaw = 0; + const auto* copied = static_cast(staging.mapped); + const auto* heldWords = static_cast(held.ptr); + for (Uint32 i = 0; i < kWords; ++i) { + if (copied[i] == pattern[i]) ++gpuRead; + if (heldWords[kFillAt / 4 + i] == kFillWord) ++heldSaw; + } + ok = gpuRead == kWords && heldSaw == kWords; + verdict = "64 KiB AHB, lock held throughout: host map read ok (" + + String(imported.coherent ? "coherent" : "non-coherent") + " memory), GPU read " + + std::to_string(gpuRead) + "/1024 words of the CPU's pattern, GPU write seen through the " + "held lock " + std::to_string(heldSaw) + "/1024 words"; + } while (false); + if (fence != VK_NULL_HANDLE) vkDestroyFence(ctx.device, fence, nullptr); + if (pool != VK_NULL_HANDLE) vkDestroyCommandPool(ctx.device, pool, nullptr); + T0DestroyImported(ctx.device, staging); + T0DestroyImported(ctx.device, imported); + MG_Remote::Transport::AdoptT0::ReleaseHeld(held); + return answer(ok, verdict); +#else + return answer(false, "no AHardwareBuffer on this platform"); +#endif + } + void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) { const auto found = m_wireBuffers.find(WireBufferKey(res)); if (found == m_wireBuffers.end()) return; - DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial); + DeferWireRelease(std::move(found->second.buffer), + found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial) + : found->second.lastUseSerial); // P11 B2, as Respecify m_wireBuffers.erase(found); ++m_sliceEpochCounter; PublishWireReclaimGauges(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 8992264b..2c53ca21 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -134,6 +134,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes); void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size); void DestroyWireBuffer(MG_Pipe::MGPipeHandle res); + // P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer + // `res`'s store - a VkBuffer on memory imported through + // VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every + // wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers) + // applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not + // imported (the T2 store is untouched and the client is DECLINED). + Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size); + // The POST self-test of the sustained-lock pattern on this device (see the definition). + static Bool SelfTestWireImport(char* why, Uint64 whyBytes); #endif // Recreate all per-frame transient arenas @@ -245,12 +254,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Only ranges actually submitted by resource_subdata are covered. No // shadow is retained: flush cannot replay stale bytes over GPU writes. Vector stagedCoverage; + // P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts + // no bytes (the client reads its own pages once the wait below it is done). + Bool imported = false; }; static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) { return (static_cast(res.Gen) << 32) | res.Slot; } WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res); Bool WaitForWireBufferHostAccess(WireBufferResource& resource); + // P11 B2: the serial a T0 store's release waits behind (never "destroy at once"). + Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const; // P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME // BOUNDARY. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp index 292c938a..1a86fb18 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp @@ -8,13 +8,45 @@ #include "VkBufferObject.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif + namespace MobileGL::MG_Backend::DirectVulkan { +#if MOBILEGL_BUILD_DISAGGREGATED + void VkBufferObject::MoveExternalFrom(VkBufferObject& other) { + m_device = other.m_device; + m_externalMemory = other.m_externalMemory; + m_externalAhb = other.m_externalAhb; + m_externalCoherent = other.m_externalCoherent; + other.m_device = VK_NULL_HANDLE; + other.m_externalMemory = VK_NULL_HANDLE; + other.m_externalAhb = nullptr; + other.m_externalCoherent = true; + } + + void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size, + void* mapped, Bool coherent, void* ahb) { + Destroy(); + m_device = device; + m_buffer = buffer; + m_externalMemory = memory; + m_mappedData = mapped; + m_size = size; + m_externalCoherent = coherent; + m_externalAhb = ahb; + } +#endif + VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept { m_allocator = other.m_allocator; m_buffer = other.m_buffer; m_allocation = other.m_allocation; m_mappedData = other.m_mappedData; m_size = other.m_size; +#if MOBILEGL_BUILD_DISAGGREGATED + MoveExternalFrom(other); +#endif other.m_allocator = nullptr; other.m_buffer = VK_NULL_HANDLE; @@ -35,6 +67,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_allocation = other.m_allocation; m_mappedData = other.m_mappedData; m_size = other.m_size; +#if MOBILEGL_BUILD_DISAGGREGATED + MoveExternalFrom(other); +#endif other.m_allocator = nullptr; other.m_buffer = VK_NULL_HANDLE; @@ -89,6 +124,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkBufferObject::Destroy() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_externalMemory != VK_NULL_HANDLE) { + // P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's + // deferred release, i.e. once no submission can still name it. + if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory); + if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr); + vkFreeMemory(m_device, m_externalMemory, nullptr); + MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb); + m_mappedData = nullptr; + m_buffer = VK_NULL_HANDLE; + m_externalMemory = VK_NULL_HANDLE; + m_externalAhb = nullptr; + m_device = VK_NULL_HANDLE; + m_externalCoherent = true; + m_size = 0; + return; + } +#endif Unmap(); if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) { vmaDestroyBuffer(m_allocator, m_buffer, m_allocation); @@ -117,6 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void VkBufferObject::Unmap() { +#if MOBILEGL_BUILD_DISAGGREGATED + // An imported store stays mapped for its life (Destroy unmaps it). + if (m_externalMemory != VK_NULL_HANDLE) return; +#endif if (!IsValid() || m_mappedData == nullptr) { m_mappedData = nullptr; return; @@ -143,6 +200,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { } Memcpy(static_cast(mapped) + offset, data, static_cast(size)); +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_externalMemory != VK_NULL_HANDLE) { + if (m_externalCoherent) return true; + VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; + range.memory = m_externalMemory; + range.offset = 0; + range.size = VK_WHOLE_SIZE; + return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS; + } +#endif const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size); if (flushResult != VK_SUCCESS) { MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult); @@ -167,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resolvedSize == 0) { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_externalMemory != VK_NULL_HANDLE) { + if (m_externalCoherent) return true; + VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; + range.memory = m_externalMemory; + range.offset = 0; + range.size = VK_WHOLE_SIZE; + return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS; + } +#endif const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize); if (result != VK_SUCCESS) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h index a9db779e..f6472580 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h @@ -64,7 +64,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { } void* GetMappedData() const { return m_mappedData; } Bool IsMapped() const { return m_mappedData != nullptr; } - Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; } + Bool IsValid() const { +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE; +#endif + return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + // P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer + // imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a + // dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves + // like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and + // the memory and drops `ahb` - which is why the store goes through the wire arm's + // serial-gated deferred release like every other: the AHardwareBuffer is let go only once + // no submission can still name it. `coherent` = the memory type is HOST_COHERENT. + void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size, + void* mapped, Bool coherent, void* ahb); + Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; } +#endif private: VmaAllocator m_allocator = nullptr; @@ -72,5 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { VmaAllocation m_allocation = nullptr; void* m_mappedData = nullptr; VkDeviceSize m_size = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + VkDevice m_device = VK_NULL_HANDLE; + VkDeviceMemory m_externalMemory = VK_NULL_HANDLE; + void* m_externalAhb = nullptr; + Bool m_externalCoherent = true; + void MoveExternalFrom(VkBufferObject& other); +#endif }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 8577ec05..f5ac2b47 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -15333,6 +15333,30 @@ void main() { // P7 gate 5 (g5-msprobe): which multisample depth/stencil resolve arm goes first is measured // once the device exists (ArmWireDepthResolveOrder, below), not keyed on the vendor. m_wirePreferShaderDepthResolve = false; + // P11 B2 (T0): a client's AHardwareBuffer imported as a wire buffer store. Wire arms, on + // Android, whenever the device advertises the extension - not per session: the device is + // created once per server and a later session asking T0 must find it able. Enabling it + // changes no T2 behaviour; only an import uses it. The 1.0-era dependencies are core at the + // 1.1 this device is created at, and are enabled too when advertised (the spike's set). + m_wireAhbImport = false; + m_wireGetAhbProperties = nullptr; +#if defined(__ANDROID__) + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && + m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 && + IsExtensionSupported(availableExtensions, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) && + IsExtensionSupported(availableExtensions, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME)) { + m_wireAhbImport = true; + for (const char* name : {VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME, + VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME, + VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME, + VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, + VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME, + VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME}) { + if (IsExtensionSupported(availableExtensions, name)) + EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, name); + } + } +#endif #endif if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr && getPhysicalDeviceProperties2 != nullptr) { @@ -15892,6 +15916,11 @@ void main() { m_wireCreateRenderPass2 = reinterpret_cast( vkGetDeviceProcAddr(m_device, wireDepthResolveCore ? "vkCreateRenderPass2" : "vkCreateRenderPass2KHR")); } + if (m_wireAhbImport) { + m_wireGetAhbProperties = + reinterpret_cast(vkGetDeviceProcAddr(m_device, "vkGetAndroidHardwareBufferPropertiesANDROID")); + if (m_wireGetAhbProperties == nullptr) m_wireAhbImport = false; + } #endif s_vkCmdDrawIndexedIndirectCount = reinterpret_cast( @@ -16733,6 +16762,19 @@ void main() { return m_instance; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool VulkanRenderer::GetWireAhbImport(WireAhbImport& out) const { + out = WireAhbImport{}; + if (!m_wireAhbImport || m_device == VK_NULL_HANDLE || m_wireGetAhbProperties == nullptr) return false; + out.device = m_device; + out.queue = m_graphicsQueue; + out.queueFamily = static_cast(m_physicalDevice.queueFamilies.graphicsFamily); + vkGetPhysicalDeviceMemoryProperties(m_physicalDevice.handle, &out.memory); + out.getAhbProperties = m_wireGetAhbProperties; + return true; + } +#endif + Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const { return m_drawIndirectCountExtensionEnabled; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 8c9f8f58..08a2b871 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -160,6 +160,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { // GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded - // true for a fence taken at that instant, wrong for work about to be recorded. Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; } + // P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from + // this device. False when the device did not take VK_ANDROID_external_memory_android_ + // hardware_buffer at creation (not Android, not advertised, or a monolith device). + struct WireAhbImport { + VkDevice device = VK_NULL_HANDLE; + VkQueue queue = VK_NULL_HANDLE; + Uint32 queueFamily = 0; + VkPhysicalDeviceMemoryProperties memory{}; + void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID + }; + Bool GetWireAhbImport(WireAhbImport& out) const; #endif // FrameContext::IRecordingObserver: prepares the frame's timer-query @@ -574,6 +585,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // target unwritten while the shader control resolved - the shader arm then resolves // first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe). Bool m_wirePreferShaderDepthResolve = false; + // P11 B2 (T0): the device took the AHardwareBuffer import extensions at creation, and the + // one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID). + Bool m_wireAhbImport = false; + void* m_wireGetAhbProperties = nullptr; // Runs the probe (memoized per device identity) and sets the member above. Called at the end of // device creation, after ArmPrimGenReroute: it records on m_graphicsQueue. void ArmWireDepthResolveOrder();