[Fix, Test] (MG_State, MG_Backend, MG_Impl, MG_Test): review wave - hand the mapping back instead of renewing it, retire an immutable ES store on respecify, and tag glTexStorage2D levels too

This commit is contained in:
2026-08-12 10:32:39 -04:00
parent 7ccb762936
commit 1b05a84928
9 changed files with 474 additions and 62 deletions
+57 -18
View File
@@ -358,8 +358,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Require working fences: recycling is gated on the frame-completion
// watermark, which only advances if Present can insert/poll fences.
return g_GLESFuncs.glFenceSync != nullptr && g_GLESFuncs.glGetSynciv != nullptr &&
r.id != 0 && !r.persistentMapped && r.contextGeneration == g_bufferContextGeneration &&
r.storageInitialized && r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes;
r.id != 0 && !r.persistentMapped && !r.immutableStorage &&
r.contextGeneration == g_bufferContextGeneration && r.storageInitialized &&
r.storageSize > 0 && r.storageSize <= kMaxPoolableBufferBytes;
}
// Retire a buffer id into the pool (owning thread; caller verified IsPoolable).
@@ -555,6 +556,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
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) {
@@ -564,9 +576,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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) {
ScrubBufferBindingShadowsForId(resource->id);
NoteBufferIdDeleted(resource->id);
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
g_GLESFuncs.glGenBuffers(1, &resource->id);
if (resource->id == 0) return nullptr;
@@ -578,6 +591,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
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) {
@@ -603,29 +620,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
void Ops_Respecify(BufferObject& bufferObject) {
auto* resource = ResourceOf(bufferObject);
if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation
if (resource->persistentMapped) {
// The frontend writes straight into the storage mapped here, and it
// renewed that mapping for the redefined store before writing to it
// (BufferObject::RedefineStorage), so the new contents already are
// where a respecification would put them.
if (bufferObject.IsBackendPersistentMapped()) return;
// Renewal declined (a zero-sized store, or the map could not be
// retaken): the buffer is back on its CPU shadow and needs an ordinary
// store again. Not this id's, though - it carries IMMUTABLE storage
// (glBufferStorageEXT), which glBufferData below would refuse. Drop it
// and let the lazy EnsureBufferResource path mint a mutable one with a
// full upload; nothing points into the old mapping any more, because
// the frontend has just given the adoption back.
// 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) {
ScrubBufferBindingShadowsForId(resource->id);
NoteBufferIdDeleted(resource->id);
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
}
resource->id = 0;
// 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();
return;
}
if (!CanTouchGLNow() || resource->id == 0 ||
resource->contextGeneration != g_bufferContextGeneration) {
@@ -922,6 +946,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// frontend re-acquires a fresh one on its next map.
resource->persistentMapped = false;
resource->persistentPtr = nullptr;
resource->immutableStorage = false;
}
// An immutable store nothing maps any more: a respecification of a buffer that
// had been persistently mapped, which Ops_Respecify could not retire because it
// ran off the context thread. glBufferData cannot redefine it, so it is retired
// here, on the thread that can, and the id is re-minted below.
if (resource->immutableStorage && !resource->persistentMapped && resource->id != 0) {
NoteBufferIdDeleted(resource->id);
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
resource->immutableStorage = false;
resource->storageInitialized = false;
resource->storageSize = 0;
resource->pendingRespecify = true;
}
// Zero-copy coherent persistent buffer: the app writes straight into the
@@ -286,6 +286,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// context loss.
Bool persistentMapped = false;
void* persistentPtr = nullptr;
// The GL store behind `id` was created with glBufferStorageEXT and is
// therefore IMMUTABLE - glBufferData cannot respecify it and it must never be
// recycled through the size-keyed buffer pool. Tracked separately from
// persistentMapped because the two come apart: a glMapBufferRange that fails
// after its glBufferStorageEXT succeeded leaves immutable storage behind with
// no map, and a respecification then has to retire the id rather than hand it
// to glBufferData, which the driver would silently refuse.
Bool immutableStorage = false;
};
// Registered as the frontend's BufferBackendOps at backend init and on
@@ -379,23 +379,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BumpSliceEpoch(*resource);
// Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0;
if (resource->persistentMapped) {
if (bufferObject.IsBackendPersistentMapped()) {
// The frontend renewed its adoption of this storage for the redefined
// store before writing a byte of it (BufferObject::RedefineStorage), so
// the new contents are already HERE and there is no second copy to
// update. Swapping the storage is what must not happen: the mapping the
// frontend holds, and every read that resolves through it, would keep
// addressing the storage being released - which is how a transform
// feedback capture came to be written to one buffer and read back out
// of another.
return;
}
// The renewal did not happen (a zero-sized store, or the storage could not
// be created): the frontend is back on its CPU shadow, so this is an
// ordinary resident buffer again and the handling below applies.
resource->persistentMapped = false;
}
// 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
}
@@ -3639,6 +3639,20 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// Once per process: the call is about to succeed, and what it does is narrower than what
// an application has every right to expect from it. Before this existed the call answered
// GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
// sampled texels untouched is the kind of thing that costs a day to find from the other
// end. MGLOG_I, not _W: warnings are compiled out at the level everything ships at.
static std::atomic<Bool> announcedNoCodec{false};
if (!announcedNoCodec.exchange(true)) {
MGLOG_I("%s: the compressed blocks are stored verbatim and returned by "
"glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
"reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
"that.",
__func__);
}
// The level's compressed image is stored as one blob, so the rectangle is patched into
// a copy of it and the whole thing handed back. Compressed sub-image uploads are not a
// hot path, and this keeps the storage layer's compressed API to the two calls it has.
@@ -4278,6 +4292,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// core 8.19). Allocating only the primary one left the object cube-incomplete, so every
// framebuffer it was attached to reported GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. Every other
// 2D target has exactly one upload target, so this loop is a no-op change for them.
// A specific compressed internalformat commits every level it allocates to that
// format, the same way glTexImage2D does - and here it matters twice over, because
// immutable storage plus glCompressedTexSubImage2D IS the modern way to upload a
// compressed texture: without the tag that sub-image call finds an uncompressed
// level and refuses it. Zero width means a generic (implementation's choice)
// format, which MobileGL answers with uncompressed storage, so it is not tagged.
const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
for (const auto uploadTarget : textureObject->GetUploadTargets()) {
for (GLsizei level = 0; level < levels; ++level) {
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
@@ -4286,6 +4307,13 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
if (compressedInfo.blockWidth != 0) {
// After AllocateStorage, which clears the tag.
textureMipmapObject->SetMipmapCompressedImage(
uploadTarget, static_cast<Uint>(level), internalformat, nullptr,
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
{levelWidth, levelHeight, 1}));
}
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
@@ -77,8 +77,12 @@ namespace MGITest {
declarations += "in int a_" + std::to_string(i) + ";\n";
copies += " sum += a_" + std::to_string(i) + ";\n";
}
const std::string vertexSource = "#version 450\n\n" + declarations + "out int sum;\n\nvoid main()\n{\n" +
copies + "}\n";
// `flat` where the CTS case has none: an integral shader output cannot be
// interpolated, so a driver is within its rights to reject the unqualified form
// even with no matching fragment input. The capture reads the same value either
// way, and the qualifier keeps this scenario portable off llvmpipe.
const std::string vertexSource = "#version 450\n\n" + declarations +
"flat out int sum;\n\nvoid main()\n{\n" + copies + "}\n";
const std::string fragmentSource = R"(#version 450
out vec4 color;
@@ -27,6 +27,7 @@
// rather than a parameter: it is the configuration that already worked, so it
// has to keep working for the others to mean anything.
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
@@ -157,7 +158,11 @@ void main() {
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
const float expected = value + static_cast<float>(i);
const float got = data[i];
if (got - expected > 0.01f || expected - got > 0.01f) {
// isfinite first: every ordered comparison against a NaN is false, so a
// pair of one-sided range tests REPORTS SUCCESS for uninitialised
// storage that happens to read as NaN - which is exactly the failure
// these scenarios exist to catch.
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
return ::testing::AssertionFailure()
<< "component " << i << " is " << got << ", expected " << expected
<< (got == kPoison ? " (the capture never reached these bytes)" : "");
@@ -281,7 +286,12 @@ void main() {
GLuint xfbBuffer = 0;
glGenBuffers(1, &xfbBuffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, nullptr, GL_MAP_READ_BIT);
// Poisoned at creation - the storage is immutable, so this is the only chance to
// put a recognisable value there, and without it a span that captured nothing
// would be indistinguishable from one that captured the right thing whenever the
// untouched bytes happened to read back as the expected number.
const std::vector<float> poison(kCaptureFloats, kPoison);
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_MAP_READ_BIT);
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferStorage on the capture buffer";
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
@@ -76,33 +76,36 @@ namespace MobileGL::MG_State::GLState {
}
// 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
// Sizing the shadow is all that takes for a shadow-backed buffer. A buffer whose
// bytes were adopted into backend GPU memory has to give the adoption back first,
// because 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.
// 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. That was the
// transform feedback capture that wrote one buffer while the readback read another.
//
// 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).
// Given back rather than renewed here, deliberately. Renewing in place would mean
// memcpying the new contents into storage that submitted-but-unretired draws may
// still be reading, which is precisely what the orphaning idiom exists to avoid;
// avoiding THAT would mean either stalling on a fence in the middle of a frame or
// teaching the persistent-map op to orphan, and the op must never orphan for the
// other kind of caller (an application-held GL_MAP_PERSISTENT_BIT mapping, whose
// pointer has to stay valid for the buffer's whole life). Handing the store back to
// the CPU shadow needs none of that: the backend's ordinary respecification path
// then does the busy-tracking and the conditional orphan it has always done, and the
// next binding that wants GPU residency takes a fresh mapping of the new store.
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();
if (m_resource.IsGpuResident()) {
m_resource.ReleasePersistentMap();
// Whatever a shader or a capture wrote is in the store being replaced, so
// there is nothing left to reconcile - and leaving the flag set would make
// the next read of this buffer wait for GPU work on behalf of bytes the
// application has just thrown away.
m_gpuWritePending = false;
}
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) {
+185
View File
@@ -1610,3 +1610,188 @@ TEST_F(GeneralBufferTest, General_CoherentAsFlush_PersistentMapAdoptsZeroCopyBac
EXPECT_EQ(GetError(), GL_NO_ERROR);
g_zeroCopyMock = nullptr;
}
// A buffer whose bytes the backend adopted into its own GPU memory - which is what
// EnsureGpuResidentStorage does for a transform-feedback capture target or a shader
// storage binding, so that MapBuffer/GetBufferSubData read real GPU results - and which
// the application then REDEFINES.
//
// The store the adopted mapping describes is the one being thrown away. Keeping that
// mapping across the redefinition is what let a transform feedback capture be written to
// one buffer and read back out of another: the backend replaced the storage (a
// respecification is the orphaning point) while the frontend went on resolving every read
// through a mapping of the storage it had just released. Two capture spans into one
// re-specified buffer came back empty from the second one onwards.
//
// So the mapping is handed back and the buffer returns to the CPU-shadow model until
// something asks for residency again. These pin all three parts of that: the adoption
// really is dropped, the new contents really do land where later reads resolve, and the
// backend really is told to respecify - it must not skip the storage, or its copy would
// keep the old bytes.
TEST_F(BufferTest, RedefiningAnAdoptedBufferHandsTheMappingBack) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
// The backend adopts the bytes, exactly as a capture target or an SSBO binding does.
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
ASSERT_EQ(static_cast<const void*>(bufferObject->MappedData()),
static_cast<const void*>(mock.gpu.data()));
mock.respecifyCalls = 0;
const GLint after[4] = {10, 20, 30, 40};
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_NE(static_cast<const void*>(bufferObject->MappedData()),
static_cast<const void*>(mock.gpu.data()));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
// The backend has a separate copy again, so it must have been told to refresh it.
EXPECT_EQ(mock.respecifyCalls, 1);
g_zeroCopyMock = nullptr;
}
// The same redefinition at a LARGER size, which is the case nothing could paper over: the
// adopted mapping is exactly as big as the old store, so writing the new contents through
// it ran past the end of the backend allocation.
TEST_F(BufferTest, RedefiningAnAdoptedBufferAtANewSizeStaysInBounds) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint small[2] = {1, 2};
BufferData(GL_ARRAY_BUFFER, sizeof(small), small, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_EQ(mock.gpu.size(), sizeof(small));
const GLint large[8] = {1, 2, 3, 4, 5, 6, 7, 8};
BufferData(GL_ARRAY_BUFFER, sizeof(large), large, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(bufferObject->GetSize(), sizeof(large));
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
// The old, smaller GPU block was not written through: still the old size, still the
// old bytes.
EXPECT_EQ(mock.gpu.size(), sizeof(small));
EXPECT_EQ(std::memcmp(mock.gpu.data(), small, sizeof(small)), 0);
// And residency can be taken again, now over the new store.
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
EXPECT_TRUE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(mock.gpu.size(), sizeof(large));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), large, sizeof(large)), 0);
g_zeroCopyMock = nullptr;
}
// glBufferStorage is the other way into a redefinition, and an adopted buffer can reach
// it: the adoption came from a binding rather than from an application map, so the buffer
// is still mutable and glBufferStorage is still legal on it.
TEST_F(BufferTest, ImmutableStorageOnAnAdoptedBufferHandsTheMappingBackToo) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
const GLint after[6] = {9, 8, 7, 6, 5, 4};
BufferStorage(GL_ARRAY_BUFFER, sizeof(after), after, GL_MAP_READ_BIT);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_TRUE(bufferObject->IsImmutableStorage());
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(bufferObject->GetSize(), sizeof(after));
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
g_zeroCopyMock = nullptr;
}
// A redefinition to nothing. The backend declines residency for an empty store, so this
// is also the path where the mapping is given back and never retaken.
TEST_F(BufferTest, RedefiningAnAdoptedBufferToZeroBytesLeavesItOnTheShadow) {
ZeroCopyMockBackend mock;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
ASSERT_TRUE(bufferObject->EnsureGpuResidentStorage());
ASSERT_TRUE(bufferObject->IsBackendPersistentMapped());
BufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(bufferObject->GetSize(), 0u);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage()); // nothing to make resident
// ...and it comes back to life on the next non-empty store.
const GLint again[3] = {5, 6, 7};
BufferData(GL_ARRAY_BUFFER, sizeof(again), again, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_TRUE(bufferObject->EnsureGpuResidentStorage());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), again, sizeof(again)), 0);
g_zeroCopyMock = nullptr;
}
// The negative control for the four above: a backend that DECLINES to hand out a mapping
// leaves the buffer shadow-backed throughout, so a redefinition is just a redefinition -
// no adoption to give back, and the backend still gets its Respecify.
TEST_F(BufferTest, RedefiningANonAdoptedBufferIsUnchanged) {
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
const GLint before[4] = {1, 2, 3, 4};
BufferData(GL_ARRAY_BUFFER, sizeof(before), before, GL_DYNAMIC_DRAW);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
mock.respecifyCalls = 0;
const GLint after[4] = {10, 20, 30, 40};
BufferData(GL_ARRAY_BUFFER, sizeof(after), after, GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
EXPECT_EQ(std::memcmp(bufferObject->MappedData(), after, sizeof(after)), 0);
EXPECT_EQ(mock.respecifyCalls, 1);
g_zeroCopyMock = nullptr;
}
+149 -3
View File
@@ -1702,6 +1702,94 @@ TEST_F(TextureTest, CompressedTexSubImage2DReplacesTheStoredBlocks) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The Y axis of the placement, which the whole-image and single-column cases above cannot see: an
// implementation that dropped the first-block-row term, or that divided yoffset by the block WIDTH,
// passes every one of them. The region here starts at block row 1, so its two blocks belong at
// bytes 16 and 24 and nowhere else.
TEST_F(TextureTest, CompressedTexSubImage2DPlacesTheFirstBlockRow) {
const GLuint texture = MakeCompressedRgtc1Texture8x8();
Uint8 whole[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The bottom block row only: 8 texels wide, 4 high, starting at y = 4.
const Uint8 bottom[16] = {0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7};
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 4, 8, 4, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(bottom)), bottom);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kRgtc1Size8x8];
std::memcpy(expected, whole, sizeof(expected));
std::memcpy(expected + 16, bottom, sizeof(bottom));
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
// And one block in the far corner, which needs both terms at once.
const Uint8 corner[8] = {0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7};
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 4, 4, 4, 4, GL_COMPRESSED_RED_RGTC1,
static_cast<GLsizei>(sizeof(corner)), corner);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
std::memcpy(expected + 24, corner, sizeof(corner));
std::memset(stored, 0, sizeof(stored));
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
(void)texture;
}
// A level whose size is neither square nor a multiple of the block size, at a level above the
// base, in a format with SIXTEEN bytes per block. Between them these pin the row stride (which a
// square level cannot distinguish from the column count), the rounding-up of a partial edge block,
// the run-to-the-edge exemption from the whole-blocks rule, and the block size actually coming from
// the format rather than from a constant.
TEST_F(TextureTest, CompressedTexSubImage2DHandlesPartialBlocksAndAMipLevel) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// 6x10 BPTC: 2 block columns x 3 block rows of 16 bytes = 96, one block row = 32.
constexpr GLsizei kBptcSize6x10 = 96;
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_BPTC_UNORM, 6, 10, 0, kBptcSize6x10,
nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
EXPECT_EQ(imageSize, kBptcSize6x10);
Uint8 whole[kBptcSize6x10];
for (Int i = 0; i < kBptcSize6x10; ++i) whole[i] = static_cast<Uint8>(i + 1);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 6, 10, GL_COMPRESSED_RGBA_BPTC_UNORM,
kBptcSize6x10, whole);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The right-hand column (2 texels wide - a partial block that runs to the edge) of the middle
// block row: one block, at byte 32 + 16.
Uint8 patch[16];
for (Int i = 0; i < 16; ++i) patch[i] = static_cast<Uint8>(0xF0 + i);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 4, 4, 2, 4, GL_COMPRESSED_RGBA_BPTC_UNORM,
static_cast<GLsizei>(sizeof(patch)), patch);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 expected[kBptcSize6x10];
std::memcpy(expected, whole, sizeof(expected));
std::memcpy(expected + 48, patch, sizeof(patch));
Uint8 stored[kBptcSize6x10] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 1, stored);
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
// The partial edge block is only exempt from the whole-blocks rule AT the edge: the same
// 2-texel width one block to the left is not.
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 4, 2, 4, GL_COMPRESSED_RGBA_BPTC_UNORM,
static_cast<GLsizei>(sizeof(patch)), patch);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
// The same call sourcing its blocks from a buffer bound to GL_PIXEL_UNPACK_BUFFER, where `data` is
// an offset into that buffer rather than a client pointer - which is the form
// KHR-GL44.buffer_storage.map_persistent_texture uses for every one of its operations.
@@ -1753,15 +1841,19 @@ TEST_F(TextureTest, CompressedUploadsAcceptAPersistentlyMappedUnpackBuffer) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, kRgtc1Size8x8, nullptr);
// The image call takes offset 0 and the sub-image call offset 128, so the readback can only
// match if the SUB-IMAGE call ran: were it refused (or a no-op), the level would still hold
// the image call's bytes.
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(0)));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "glCompressedTexImage2D over a persistent map";
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
reinterpret_cast<const void*>(static_cast<SizeT>(0)));
reinterpret_cast<const void*>(static_cast<SizeT>(128)));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "glCompressedTexSubImage2D over a persistent map";
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, source, sizeof(stored)), 0);
EXPECT_EQ(std::memcmp(stored, source + 128, sizeof(stored)), 0);
MG_Impl::GLImpl::UnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
@@ -3569,3 +3661,57 @@ TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerm
ExpectSingleGlError(GL_INVALID_OPERATION);
}
}
// Immutable storage plus glCompressedTexSubImage2D is the modern way to upload a compressed
// texture, so glTexStorage2D has to commit its levels to a specific compressed internalformat
// exactly as glTexImage2D does. When it did not, the sub-image call found an uncompressed level
// and refused it, and glTexImage2D and glTexStorage2D disagreed about the same token.
TEST_F(TextureTest, TexStorage2DTagsEveryLevelForASpecificCompressedFormat) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 2, GL_COMPRESSED_RED_RGTC1, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
for (GLint level = 0; level < 2; ++level) {
GLint compressed = GL_FALSE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_TRUE) << "level " << level;
GLint internalFormat = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat);
EXPECT_EQ(internalFormat, static_cast<GLint>(GL_COMPRESSED_RED_RGTC1)) << "level " << level;
GLint imageSize = 0;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize);
// 8x8 -> 2x2 blocks -> 32 bytes; 4x4 -> 1 block -> 8 bytes.
EXPECT_EQ(imageSize, level == 0 ? 32 : 8) << "level " << level;
}
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// ...and the sub-image call the whole arrangement exists for now reaches both levels.
Uint8 blocks[kRgtc1Size8x8];
for (Int i = 0; i < kRgtc1Size8x8; ++i) blocks[i] = static_cast<Uint8>(0x40 + i);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 8, 8, GL_COMPRESSED_RED_RGTC1, kRgtc1Size8x8,
blocks);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 stored[kRgtc1Size8x8] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_2D, 0, stored);
EXPECT_EQ(std::memcmp(stored, blocks, sizeof(stored)), 0);
MG_Impl::GLImpl::CompressedTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 4, 4, GL_COMPRESSED_RED_RGTC1, 8, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The negative control: a generic compressed token leaves glTexStorage2D's levels uncompressed,
// because for those the implementation's choice IS the answer and MobileGL chooses uncompressed.
TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED, 8, 8);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint compressed = GL_TRUE;
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
EXPECT_EQ(compressed, GL_FALSE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}