[Fix, Test] (MG_State, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_IntegrationTest): a respecified capture buffer left the transform feedback writing one store and the readback reading another

This commit is contained in:
2026-08-12 08:42:45 -04:00
parent 811f32760e
commit 0e31c1481b
7 changed files with 381 additions and 5 deletions
@@ -75,10 +75,39 @@ namespace MobileGL::MG_State::GLState {
NotifySubData(offset, size);
}
void BufferObject::Respecify(SizeT size, const void* data) {
ReleaseMemory();
// A (re)definition of the store is about to write `size` bytes through Bytes().
// Sizing the shadow is all that takes for a shadow-backed buffer, but a buffer
// whose bytes were adopted into backend GPU memory needs the adoption renewed
// first: the mapping it holds describes exactly the OLD store. Writing the new
// contents through it runs past its end the moment the store grows, and a backend
// that replaces the storage for the new store (which is what an orphaning
// respecification asks for) would leave that mapping - and therefore every later
// read of this buffer - addressing storage nothing writes to any more.
//
// Renewing rather than simply dropping is what keeps the common case free: a
// redefinition at the same size gets the same mapping back without any storage
// being created, which is also what makes the backend's respecify a no-op (the
// new bytes are already in the storage it would otherwise upload to).
void BufferObject::RedefineStorage(SizeT size) {
const Bool wasGpuResident = m_resource.IsGpuResident();
if (wasGpuResident) {
// A capture or a shader write may still be running against the very bytes
// that are about to be overwritten.
SyncGpuWrites();
m_resource.ReleasePersistentMap();
}
m_size = size;
m_resource.ResizeShadow(size);
if (wasGpuResident) {
// Declining is allowed (a zero-sized store, a backend without the op): the
// buffer simply goes back to the CPU-shadow model it had before adoption.
EnsureGpuResidentStorage();
}
}
void BufferObject::Respecify(SizeT size, const void* data) {
ReleaseMemory();
RedefineStorage(size);
if (data && size > 0) {
Memcpy(m_resource.Bytes(), data, size);
}
@@ -96,8 +125,7 @@ namespace MobileGL::MG_State::GLState {
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
ReleaseMemory();
m_size = size;
m_resource.ResizeShadow(size);
RedefineStorage(size);
if (data) {
Memcpy(m_resource.Bytes(), data, size);
} else if (size > 0) {
@@ -205,6 +205,9 @@ namespace MobileGL {
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.
void RedefineStorage(SizeT size);
void NotifyRespecify();
void NotifySubData(SizeT offset, SizeT size);
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
@@ -70,6 +70,15 @@ namespace MobileGL::MG_State::GLState {
m_shadow->shrink_to_fit();
}
// Give the adoption back: the bytes resolve against the shadow again (which
// the caller must (re)size, it was released on adoption). Used when the store
// itself is redefined - the mapping describes exactly the store that is going
// away, so it may neither be written through nor kept. It is NOT a general
// "unmap": a persistent map the application holds outlives every unmap by
// definition, and the calls that could redefine such a buffer's store are
// 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); }