mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[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).
This commit is contained in:
@@ -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<unsigned long long>(mask),
|
||||
static_cast<unsigned long long>(required),
|
||||
static_cast<unsigned long long>(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.
|
||||
//
|
||||
|
||||
@@ -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<GLESBufferResource*>(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<GLESBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
if (!resource) {
|
||||
auto created = MakeShared<GLESBufferResource>();
|
||||
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<GLsizeiptr>(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<GLsizeiptr>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(resource->pendingMutex);
|
||||
auto& write = resource->pendingResidentWrites.emplace_back();
|
||||
write.offset = offset;
|
||||
const auto* bytes = static_cast<const Uint8*>(data.data);
|
||||
write.bytes.assign(bytes, bytes + data.size);
|
||||
}
|
||||
|
||||
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> 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<std::mutex> 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<std::mutex> 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<Bool>(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<SizeT>(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<GLsizeiptr>(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<BackendBufferResource>&& resource) {
|
||||
if (!resource) return;
|
||||
auto* glesResource = static_cast<GLESBufferResource*>(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<BufferMappingAccessBit> appAccess) {
|
||||
Ops_FlushMappedRange(bufferObject, range, appAccess);
|
||||
BumpBufferMutationEpoch();
|
||||
}
|
||||
void Ops_OnDestroyTracked(SharedPtr<BackendBufferResource>&& 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<unsigned long long>(mask),
|
||||
static_cast<unsigned long long>(
|
||||
MG_Pipe::MGPipeSubsystemRequires(MG_Pipe::kMGPipeSubsystemVertexInput)),
|
||||
static_cast<unsigned long long>(
|
||||
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<GLESBufferResource*>(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()
|
||||
|
||||
@@ -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<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
|
||||
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();
|
||||
|
||||
@@ -337,34 +337,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return evicted;
|
||||
}
|
||||
|
||||
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& 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<VkRenderPass> 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();) {
|
||||
|
||||
@@ -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<VkRenderPass>& 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
|
||||
|
||||
@@ -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<VkBufferManager*> 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<BufferMappingAccessBit> 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<BackendBufferResource>&& 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<SizeT>(offset),
|
||||
static_cast<SizeT>(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<Uint64>(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<SizeT>(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<SizeT>(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<VkBufferResource*>(bufferObject.GetBackendResource().get());
|
||||
}
|
||||
|
||||
VkBufferResource* VkBufferManager::GetOrCreateResource(
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& 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<VkBufferResource*>(existing.get());
|
||||
}
|
||||
auto resource = MakeShared<VkBufferResource>();
|
||||
VkBufferResource* raw = resource.get();
|
||||
bufferObject->SetBackendResource(resource);
|
||||
TrackLiveResource(resource);
|
||||
return raw;
|
||||
}
|
||||
|
||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& 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<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
|
||||
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& 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<unsigned long long>(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<VkDeviceSize>(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<Uint64>(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<VkDeviceSize>(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<Uint64>(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<VkDeviceSize>(offset);
|
||||
region.size = static_cast<VkDeviceSize>(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<VkDeviceSize>(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<Uint64>(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<VkDeviceSize>(bufferObject.GetSize()) != resource->storageSize) {
|
||||
resource->pendingFullUpload = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsResourceBusy(*resource)) {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(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<Uint64>(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<BufferMappingAccessBit> appAccess) {
|
||||
auto* resource = ResourceOf(bufferObject);
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
BumpSliceEpoch(*resource);
|
||||
resource->transientFrameSerial = 0;
|
||||
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
||||
return;
|
||||
}
|
||||
if (static_cast<VkDeviceSize>(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<VkDeviceSize>(size), static_cast<VkDeviceSize>(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<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StagedRangeCopy(*resource, bufferObject.MappedData() + offset, offset, size)) {
|
||||
SwapStorageAndUploadAll(*resource, bufferObject);
|
||||
}
|
||||
}
|
||||
|
||||
void VkBufferManager::OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource) {
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
auto vkResource = std::static_pointer_cast<VkBufferResource>(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<VkDeviceSize>(bufferObject.GetSize());
|
||||
if (size == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto resource = std::static_pointer_cast<VkBufferResource>(bufferObject.GetBackendResource());
|
||||
if (!resource) {
|
||||
resource = MakeShared<VkBufferResource>();
|
||||
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<Uint64>(size));
|
||||
}
|
||||
}
|
||||
resource->persistentMapped = true;
|
||||
resource->pendingFullUpload = false;
|
||||
resource->storageSize = size;
|
||||
resource->lastUseSerial = 0;
|
||||
return resource->buffer.GetMappedData();
|
||||
}
|
||||
|
||||
Bool VkBufferManager::AcquireResidentSlice(BufferKind kind,
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& 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<VkDeviceSize>(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<Uint64>(size));
|
||||
}
|
||||
resource->pendingFullUpload = false;
|
||||
}
|
||||
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
outSlice = resource->buffer.GetSlice(0, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::AcquireStreamedSlice(BufferKind kind,
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& 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<VkDeviceSize>(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<VkDeviceSize>(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<Uint64>(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
|
||||
|
||||
@@ -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<MG_State::GLState::BufferObject>& 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<MG_State::GLState::BufferObject>& 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<BufferMappingAccessBit> appAccess);
|
||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& 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<MG_State::GLState::BufferObject>& 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<VkBufferResource>& 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<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||
std::unordered_map<Uint64, WireBufferResource> 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
|
||||
|
||||
@@ -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<Uint32>(target) - static_cast<Uint32>(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<std::mutex> lock(m_mutex);
|
||||
m_pendingClears.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -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<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
const SizeT textureLifetimeHash = std::hash<Uint64>{}(key.textureLifetimeId);
|
||||
const SizeT mipHash = std::hash<Uint32>{}(key.mipLevel);
|
||||
const SizeT layerHash = std::hash<Uint32>{}(key.baseArrayLayer);
|
||||
const SizeT layerCountHash = std::hash<Uint32>{}(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<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
hash ^= std::hash<Uint64>{}(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<Uint32> m_pendingCount{0};
|
||||
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -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<Uint64>(8, static_cast<Uint64>(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<VkRenderPass> 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<VkRenderPass> 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<TrackedAttachmentLayoutInfo> 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<Int>(trackedAttachment.target));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,40 +20,6 @@
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
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<MG_State::GLState::ITextureObject> 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<MG_State::GLState::RenderbufferObject> 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<VkDevice> s_device;
|
||||
static inline thread_local Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
|
||||
Uint64 hash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
Uint64 compatibilityHash = 0;
|
||||
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
|
||||
Vector<TrackedAttachmentLayoutInfo> 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<PendingClearAttachmentInfo>& pendingClearAttachments,
|
||||
const Vector<TrackedAttachmentLayoutInfo>& 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<TrackedAttachmentLayoutInfo> 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<VkRenderPass>& 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<Uint64, RenderPassEntry> 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<MG_State::GLState::RenderbufferObject> 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<MG_State::GLState::RenderbufferObject> 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<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
|
||||
UnorderedMap<VkFormat, VkSampleCountFlags> 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<ActiveRenderPassInfo> s_activeRenderPass;
|
||||
static inline SessionLocal<Bool> s_hasActiveRenderPass;
|
||||
static inline SessionLocal<VkClearManager*> s_clearManager;
|
||||
static inline SessionLocal<VkTextureManager*> s_textureManager;
|
||||
static inline SessionLocal<SwapchainObject*> s_swapchainObject;
|
||||
static inline SessionLocal<VkRenderPassManager*> s_renderPassManager;
|
||||
|
||||
@@ -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<TextureIdentity> 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<MG_State::GLState::ITextureObject*> 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<TextureIdentity> 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<Uint32>(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:
|
||||
|
||||
@@ -108,27 +108,6 @@ inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAt
|
||||
return static_cast<Uint32>(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<Uint32>(glLevel > 0 ? glLevel : 0);
|
||||
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
|
||||
}
|
||||
|
||||
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
|
||||
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
|
||||
return texture != nullptr ? layer + static_cast<Uint32>(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<MG_State::GLState::ITextureObject*>{}(key.texture);
|
||||
hash ^= std::hash<Uint64>{}(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<Uint8>& 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<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> 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<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> 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<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
|
||||
// Supported multisample counts per format, so repeat texture syncs do not
|
||||
// re-query vkGetPhysicalDeviceImageFormatProperties.
|
||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||
|
||||
@@ -2222,12 +2222,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_sampledReadStageMask,
|
||||
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily)});
|
||||
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
|
||||
m_clearManager = MakeUnique<VkClearManager>();
|
||||
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
|
||||
succeeded = m_clearManager->Initialize();
|
||||
MOBILEGL_ASSERT(succeeded, "VkClearManager initialization failed.");
|
||||
m_renderPassManager =
|
||||
MakeUnique<VkRenderPassManager>(m_device, m_physicalDevice.handle, m_allocator, m_config, *m_clearManager,
|
||||
MakeUnique<VkRenderPassManager>(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<VkSamplerManager>();
|
||||
@@ -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<GLint>(ToStorageMipLevel(srcEndpoint.Texture.get(), srcLevel));
|
||||
dstLevel = static_cast<GLint>(ToStorageMipLevel(dstEndpoint.Texture.get(), dstLevel));
|
||||
srcZ = static_cast<GLint>(ToStorageArrayLayer(srcEndpoint.Texture.get(), srcZ));
|
||||
dstZ = static_cast<GLint>(ToStorageArrayLayer(dstEndpoint.Texture.get(), dstZ));
|
||||
srcLevel = std::max<GLint>(srcLevel, 0);
|
||||
dstLevel = std::max<GLint>(dstLevel, 0);
|
||||
srcZ = std::max<GLint>(srcZ, 0);
|
||||
dstZ = std::max<GLint>(dstZ, 0);
|
||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
|
||||
static_cast<Uint32>(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<VkRenderPass>& 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
|
||||
|
||||
@@ -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<VkRenderPass>& 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<ProgramFactory> m_programFactory;
|
||||
UniquePtr<UniformManager> m_uniformManager;
|
||||
UniquePtr<VertexInputStateFactory> m_vertexInputStateFactory;
|
||||
UniquePtr<VkClearManager> m_clearManager;
|
||||
UniquePtr<VkRenderPassManager> m_renderPassManager;
|
||||
UniquePtr<VkTextureManager> m_textureManager;
|
||||
UniquePtr<VkSamplerManager> m_samplerManager;
|
||||
|
||||
@@ -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<Uint64> 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<BufferMappingAccessBit>, unnormalised");
|
||||
MG_Pipe::MGPipeEmitResourceFlushRange(*this, range.start, range.end - range.start,
|
||||
static_cast<Uint32>(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<Uint8> 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<BufferMappingAccessBit> 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<BackendBufferResource>& BufferObject::GetBackendResource() const {
|
||||
return m_resource.Backend();
|
||||
}
|
||||
|
||||
void BufferObject::SetBackendResource(SharedPtr<BackendBufferResource> resource) {
|
||||
m_resource.SetBackend(std::move(resource));
|
||||
}
|
||||
|
||||
Bool BufferObject::IsMapped() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RefuseLegacyBufferArmFromApplyThread("IsMapped");
|
||||
|
||||
@@ -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<BufferMappingAccessBit> 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<BackendBufferResource>&& 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<BackendBufferResource>& GetBackendResource() const;
|
||||
void SetBackendResource(SharedPtr<BackendBufferResource> 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.
|
||||
|
||||
@@ -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<BackendBufferResource>& Backend() const { return m_backend; }
|
||||
void SetBackend(SharedPtr<BackendBufferResource> backend) { m_backend = std::move(backend); }
|
||||
SharedPtr<BackendBufferResource> 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<MapAlignedData> m_shadow = MakeShared<MapAlignedData>();
|
||||
void* m_gpuMapped = nullptr;
|
||||
SharedPtr<BackendBufferResource> m_backend;
|
||||
// See ShadowAllocationBytes. Split-only so the pull build's layout does not move (G1).
|
||||
SizeT m_shadowExtent = 0;
|
||||
};
|
||||
|
||||
@@ -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<SizeT>(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<SizeT>(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<BufferMappingAccessBit>) {
|
||||
void ZeroCopyMock_Flush(MG_Pipe::MGPipeHandle, const MG_Pipe::MGPFlushRange&, const void*) {
|
||||
if (g_zeroCopyMock) ++g_zeroCopyMock->flushCalls;
|
||||
}
|
||||
void ZeroCopyMock_OnDestroy(SharedPtr<MG_State::GLState::BackendBufferResource>&&) {}
|
||||
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<const Uint8*>(data.data);
|
||||
write.bytes.assign(bytes, bytes + data.size);
|
||||
write.offset = static_cast<SizeT>(MG_Pipe::MGPipeSubDataBufferOffset(record));
|
||||
const auto* first = static_cast<const Uint8*>(bytes);
|
||||
write.bytes.assign(first, first + static_cast<SizeT>(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
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
@@ -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<Uint8> minted(kSize, static_cast<Uint8>(0));
|
||||
g_mintedPersistentBase = minted.data();
|
||||
|
||||
auto adopted = MakeBuffer(27u, kSize);
|
||||
SharedPtr<BufferObject> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# base commit : <unknown>
|
||||
# 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::BackendBufferResource>)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user