mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (Magma, P11 B2): import a T0 store through VK_ANDROID_external_memory_android_hardware_buffer as a wire buffer (external VkBufferObject), serial-gated release, done-no-bytes readback, and the sustained-lock POST on the renderer's device
This commit is contained in:
@@ -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 <android/hardware_buffer.h>
|
||||
#endif
|
||||
#include <Config.h>
|
||||
#include <cstdlib>
|
||||
#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<Uint8> bytes(static_cast<SizeT>(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<Uint8> bytes(t0 ? 0 : static_cast<SizeT>(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<Int32>(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<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
|
||||
VkAndroidHardwareBufferPropertiesANDROID properties{
|
||||
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
|
||||
VkResult result = getProperties(ctx.device, static_cast<const AHardwareBuffer*>(ahb), &properties);
|
||||
if (result != VK_SUCCESS) {
|
||||
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(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<int>(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<AHardwareBuffer*>(ahb);
|
||||
import.pNext = &dedicated;
|
||||
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocateInfo.pNext = &import;
|
||||
allocateInfo.allocationSize = properties.allocationSize;
|
||||
allocateInfo.memoryTypeIndex = static_cast<Uint32>(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<int>(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<int>(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<unsigned long long>(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<SizeT>(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<unsigned long long>(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<SizeT>(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<Uint32> pattern(kBytes / 4);
|
||||
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(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<Uint32>(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<const Uint32*>(staging.mapped);
|
||||
const auto* heldWords = static_cast<const Uint32*>(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();
|
||||
|
||||
@@ -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<Range1D> 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<Uint64>(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.
|
||||
|
||||
@@ -8,13 +8,45 @@
|
||||
|
||||
#include "VkBufferObject.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Transport/AdoptT0.h>
|
||||
#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<Uint8*>(mapped) + offset, data, static_cast<SizeT>(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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PFN_vkCreateRenderPass2>(
|
||||
vkGetDeviceProcAddr(m_device, wireDepthResolveCore ? "vkCreateRenderPass2" : "vkCreateRenderPass2KHR"));
|
||||
}
|
||||
if (m_wireAhbImport) {
|
||||
m_wireGetAhbProperties =
|
||||
reinterpret_cast<void*>(vkGetDeviceProcAddr(m_device, "vkGetAndroidHardwareBufferPropertiesANDROID"));
|
||||
if (m_wireGetAhbProperties == nullptr) m_wireAhbImport = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_vkCmdDrawIndexedIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
|
||||
@@ -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<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
||||
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice.handle, &out.memory);
|
||||
out.getAhbProperties = m_wireGetAhbProperties;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const {
|
||||
return m_drawIndirectCountExtensionEnabled;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user