[Perf] (Magma): small busy wire buffers are renamed from a host shadow instead of a staged copy that ends the pass (FCL WriteWireBuffer 0.100 -> 0.077 ms/frame).

This commit is contained in:
2026-10-08 06:59:40 -04:00
parent 855b3b5009
commit 748ffc31c5
7 changed files with 358 additions and 14 deletions
+4
View File
@@ -136,6 +136,10 @@ namespace MobileGL::MG_Config {
// conversion is converted once per frame slot per (buffer content, layout) and reused by
// every draw that names it, instead of once per draw. Auto is ON; ForceOff converts per draw.
QuirkOverride MagmaVertexConversionCache = QuirkOverride::Auto;
// MOBILEGL_MAGMA_WRITE_RENAME: a glBufferSubData into a small wire store the GPU may still
// be reading gives the store a fresh buffer filled from a host shadow, instead of a staged
// copy that ends the render pass. Auto is ON; ForceOff keeps the staged copy.
QuirkOverride MagmaWriteRename = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1
View File
@@ -349,6 +349,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT");
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
@@ -238,18 +238,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->gpuWritesPending = false;
resource->indirectReadBarrierPending = false;
resource->stagedCoverage.clear();
DropWireShadow(*resource);
if (resource->size == 0) return;
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
if (!resource->buffer.Create({
.allocator = m_initInfo.allocator,
.size = resource->size,
.usage = usage,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
}) || resource->buffer.Map() == nullptr) {
if (!CreateWireStoreBuffer(*resource)) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-storage\"} {slot=%u, gen=%u, size=%llu}",
res.Slot, res.Gen, static_cast<unsigned long long>(resource->size));
std::abort();
@@ -257,6 +249,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
++m_wireStoreCount;
NoteWireStorePeaks();
PublishWireReclaimGauges();
// P15 S0 (M4): a small store starts a host shadow. Its bytes are undefined until written
// (glBufferData without data), so zeros are as good a starting copy as the new memory's.
constexpr SizeT kWireShadowMaxStore = 64u << 10;
constexpr SizeT kWireShadowBudget = 64u << 20;
if (MG_Config::Features.MagmaWriteRename != MG_Config::QuirkOverride::ForceOff &&
resource->size <= kWireShadowMaxStore && m_wireShadowBytes + resource->size <= kWireShadowBudget) {
resource->shadow.assign(static_cast<SizeT>(resource->size), 0);
m_wireShadowBytes += resource->shadow.size();
}
// Under transport, defined bytes follow as resource_subdata records. An
// undefined store has no shadow to upload and no implicit zero snapshot.
if (initialBytes != nullptr && desc.HasDefinedContent) {
@@ -264,6 +265,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
Bool VkBufferManager::CreateWireStoreBuffer(WireBufferResource& resource) {
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
return resource.buffer.Create({
.allocator = m_initInfo.allocator,
.size = resource.size,
.usage = usage,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
}) &&
resource.buffer.Map() != nullptr;
}
void VkBufferManager::DropWireShadow(WireBufferResource& resource) {
if (resource.shadow.empty()) return;
m_wireShadowBytes -= resource.shadow.size();
resource.shadow.clear();
resource.shadow.shrink_to_fit();
}
// P15 S0 (M4): WRITE RENAMING. A busy store (the GPU may still read it: an earlier draw of
// this frame, or one in flight) gets a fresh VkBuffer holding the shadow - which already has
// this write in it - and the old one is released once everything that can name it completes,
// exactly as a glBufferData orphan is. Draws recorded before keep the old buffer and its old
// bytes; draws after bind the new one (every bind site compares VkBuffer handles, and the
// release goes through the destroy epoch the handle-keyed memos fold in). No copy is recorded,
// so the render pass stays open and no barrier is needed.
Bool VkBufferManager::RenameBusyWireStore(WireBufferResource& resource) {
// A shadow outlives only GPU writes whose bytes it has (this manager's own staged copies):
// every other GPU write drops it. So a pending GPU write does not bar the rename.
if (resource.shadow.size() != resource.size || resource.imported ||
MG_Config::Features.MagmaWriteRename == MG_Config::QuirkOverride::ForceOff)
return false;
VkBufferObject old = std::move(resource.buffer);
if (!CreateWireStoreBuffer(resource) ||
!resource.buffer.Upload(resource.shadow.data(), static_cast<VkDeviceSize>(resource.size), 0)) {
resource.buffer.Destroy();
resource.buffer = std::move(old);
return false;
}
// Busy means a GPU command recorded so far may name the old buffer: release it behind
// this frame's serial and the current sync point, whatever its own stamps said.
DeferWireRelease(std::move(old), m_frameSerial);
resource.lastUseSerial = 0;
resource.lastUseSubmitIndex = 0;
resource.gpuWritesPending = false;
++m_wireStoreCount;
++m_wireRenames;
NoteWireStorePeaks();
PublishWireReclaimGauges();
return true;
}
Bool VkBufferManager::WaitForWireBufferHostAccess(WireBufferResource& resource) {
// B3 review: the early return must consult the same submission term WriteWireBuffer
// does, or a caller that reaches here with the serial term idle skips the wait the
@@ -314,6 +369,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (size == 0) return;
resource->contentSerial = ++m_wireContentSerial;
if (!resource->shadow.empty())
Memcpy(resource->shadow.data() + offset, bytes, static_cast<SizeT>(size));
Bool uploaded = false;
const Uint64 probeLastUse = resource->lastUseSerial;
const Uint64 probeCompleted = GetCompletedSerial();
@@ -360,7 +417,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool busyBySubmission =
pVulkanRenderer != nullptr && !pVulkanRenderer->IsSubmitIndexComplete(resource->lastUseSubmitIndex);
if (busyBySerial || busyBySubmission) {
if ((busyBySerial || busyBySubmission) && !resource->shadow.empty() && RenameBusyWireStore(*resource)) {
uploaded = true; // the new buffer was filled from the shadow, this write included
} else if (busyBySerial || busyBySubmission) {
// Vulkan buffer copies require four-byte aligned ranges. An odd GL
// byte update waits, then writes only the exact range; rounding it
// from a CPU shadow could overwrite neighbouring GPU-written bytes.
@@ -502,8 +561,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!resource || !resource->buffer.IsValid() || resource->size == 0) return false;
resource->lastUseSerial = m_frameSerial;
// A slice a shader may write through: whatever it writes is a new content state.
if (kind == BufferKind::ShaderStorage || kind == BufferKind::TextureBuffer)
if (kind == BufferKind::ShaderStorage || kind == BufferKind::TextureBuffer) {
resource->contentSerial = ++m_wireContentSerial;
DropWireShadow(*resource);
}
// B3: the draw this slice is being acquired for is normally recorded into the NEXT
// submission - normally, not always: a mid-draw flush (SyncWireTextureShape's preserve
// path) can submit this index without the draw. This stamp is the defence term's input
@@ -524,6 +585,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* resource = FindWireBuffer(res);
if (!resource || offset > resource->size || size > resource->size - offset || (size && !dst)) return false;
if (size == 0) return true;
// A shadowed store's host copy is current, staged copies included (see RenameBusyWireStore).
if (resource->shadow.size() == resource->size) {
Memcpy(dst, resource->shadow.data() + offset, static_cast<SizeT>(size));
return true;
}
// Concurrent GPU reads do not prevent a CPU read. Only staged copies or
// shader writes need a host visibility barrier and a submission wait;
// ordinary index/vertex inspection must not stall once per draw.
@@ -663,6 +729,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (size == MG_Pipe::kMGPipeWholeBuffer) size = resource->size - offset;
if (size == 0 || size > resource->size - offset) return;
resource->contentSerial = ++m_wireContentSerial;
DropWireShadow(*resource);
resource->lastUseSerial = m_frameSerial;
resource->stagedCoverage.clear();
resource->gpuWritesPending = true;
@@ -804,6 +871,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (mapped == nullptr) return nullptr;
if (resource->imported) return mapped; // already donated: the same pointer
resource->contentSerial = ++m_wireContentSerial;
DropWireShadow(*resource);
if (!WaitForWireBufferHostAccess(*resource)) return nullptr;
if (seedBytes != nullptr) std::memcpy(mapped, seedBytes, static_cast<SizeT>(size));
resource->imported = true;
@@ -838,6 +906,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->buffer.AdoptExternal(ctx.device, imported.buffer, imported.memory, size, imported.mapped,
imported.coherent, ahb);
resource->imported = true;
DropWireShadow(*resource);
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0;
resource->gpuWritesPending = false;
@@ -1084,6 +1153,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return;
DropWireShadow(found->second);
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
@@ -1261,6 +1331,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch;
m_wireBufferLookup.clear();
m_wireBuffers.clear();
m_wireShadowBytes = 0;
m_wireStoreCount = 0;
m_wireStoreCountPeak = 0;
m_deferredWireBytesPeak = 0;
@@ -84,6 +84,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// P15 S0 (M4): busy writes that renamed their store instead of taking a staged copy.
Uint64 GetWireRenameCount() const { return m_wireRenames; }
// Resource-op entry points. All run on the server apply owner.
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
@@ -197,6 +199,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 lastUseSubmitIndex = 0;
// P15 S0: see GetWireBufferContentSerial.
Uint64 contentSerial = 0;
// P15 S0 (M4): the store's bytes on the host, for a small store whose content only
// host writes have made since it was specified - what a busy write renames from (see
// WriteWireBuffer). Empty: no shadow (a large store, the budget was spent, or a GPU
// write, shader-writable bind or client mapping made the host copy unknowable).
Vector<Uint8> shadow;
Bool gpuWritesPending = false;
// Only ranges actually submitted by resource_subdata are covered. No
// shadow is retained: flush cannot replay stale bytes over GPU writes.
@@ -210,6 +217,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
Uint64 m_wireRenames = 0;
SizeT m_wireShadowBytes = 0;
void DropWireShadow(WireBufferResource& resource);
Bool CreateWireStoreBuffer(WireBufferResource& resource);
Bool RenameBusyWireStore(WireBufferResource& resource);
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
}
@@ -2371,8 +2371,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
WireDeclineTally::Dump("shutdown");
// The SPIR-V screen's engagement proof: a decline-free session above reads as "screened
// and valid" only with this count beside it (distinct modules, process-wide).
MGLOG_I("MGWIRE-VCONV[shutdown] converted=%llu reused=%llu", static_cast<unsigned long long>(m_wireStreamsConverted),
static_cast<unsigned long long>(m_wireStreamsReused));
MGLOG_I("MGWIRE-VCONV[shutdown] converted=%llu reused=%llu renamedWrites=%llu",
static_cast<unsigned long long>(m_wireStreamsConverted),
static_cast<unsigned long long>(m_wireStreamsReused),
static_cast<unsigned long long>(m_bufferManager.GetWireRenameCount()));
MGLOG_I("MGWIRE-SPIRV-SCREEN[shutdown] modulesValidated=%llu",
static_cast<unsigned long long>(ProgramFactory::DriverModuleValidatorRuns()));
DestroyWireDrawPass();
@@ -100,6 +100,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/InPassClearScenario.cpp
# P15 S0: a CPU-converted vertex stream reused and re-written within a frame (DirectVulkan caches it).
Scenarios/ConvertedVertexStreamScenario.cpp
Scenarios/WriteRenameScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
@@ -2458,6 +2459,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_register_split_arms("ClearThenReadPixelsScenario.*" "")
mgl_itest_register_split_arms("InPassClearScenario.*" "")
mgl_itest_register_split_arms("ConvertedVertexStreamScenario.*" "")
mgl_itest_register_split_arms("WriteRenameScenario.*" "")
# GL_ARB_shader_objects (P13): ARB-created shader / program objects travel the record arm like
# core-created ones, so the scenario runs on every split arm as well as both monoliths.
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
@@ -0,0 +1,252 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WriteRenameScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glBufferSubData INTO A SMALL BUFFER AN EARLIER DRAW OF THE SAME FRAME STILL READS.
//
// P15 S0 (M4): DirectVulkan's wire arm answers such a write by giving the store a fresh buffer
// filled from a host shadow (write renaming) instead of a staged copy that ends the render pass.
// What renaming must get right:
//
// * ORDER: the draw before the write reads the old bytes, the draw after reads the new ones.
// * CARRY: bytes the write did not touch are in the new buffer (the shadow holds them).
// * EVERY BIND SITE sees the new buffer: vertex, index and uniform buffers.
// * ANY ALIGNMENT: a one-byte write renames too (the staged copy needed four-byte ranges).
// * ACROSS FRAMES: a renamed store keeps drawing right in later frames.
//
// DirectGLES is the control; MOBILEGL_MAGMA_WRITE_RENAME=0 takes the staged copy.
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// Position in [0, 1] and a per-vertex colour.
constexpr const char* kVS = R"(#version 330 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec4 aColor;
out vec4 vColor;
void main() { vColor = aColor; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }
)";
constexpr const char* kFS = R"(#version 330 core
in vec4 vColor;
layout(std140) uniform Tint { vec4 uTint; };
out vec4 o0;
void main() { o0 = vColor * uTint; }
)";
constexpr int kSize = 64;
struct Vertex {
float x, y;
unsigned char r, g, b, a;
};
static_assert(sizeof(Vertex) == 12, "tight vertex");
class WriteRenameScenario : public ScenarioTest {};
// Four vertices of an axis-aligned quad as a triangle strip.
void PutQuad(std::vector<Vertex>& v, int first, float x0, float y0, float x1, float y1, unsigned char r,
unsigned char g, unsigned char b) {
v[first + 0] = {x0, y0, r, g, b, 255};
v[first + 1] = {x1, y0, r, g, b, 255};
v[first + 2] = {x0, y1, r, g, b, 255};
v[first + 3] = {x1, y1, r, g, b, 255};
}
::testing::AssertionResult Near(const Rgba8& got, int r, int g, int b) {
const auto close = [](int v, int want) { return v >= want - 3 && v <= want + 3; };
if (close(got.r, r) && close(got.g, g) && close(got.b, b)) return ::testing::AssertionSuccess();
return ::testing::AssertionFailure() << "got (" << int(got.r) << ", " << int(got.g) << ", " << int(got.b)
<< "), want (" << r << ", " << g << ", " << b << ")";
}
Rgba8 At(int x, int y) { return ReadPixelsRect(x, y, 1, 1).At(0, 0); }
struct Scene {
GLuint program = 0, fbo = 0, color = 0, vao = 0, vbo = 0, ibo = 0, ubo = 0;
};
// A target, the program, a VAO over `vertices` (float2 position + ubyte4 colour), a
// four-index strip index buffer and a white tint.
Scene MakeScene(const std::vector<Vertex>& vertices, std::string* error) {
Scene s;
s.program = CompileProgram(kVS, kFS, error);
if (s.program == 0) return s;
glUniformBlockBinding(s.program, glGetUniformBlockIndex(s.program, "Tint"), 0);
glGenTextures(1, &s.color);
glBindTexture(GL_TEXTURE_2D, s.color);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
glGenFramebuffers(1, &s.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, s.fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s.color, 0);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glGenVertexArrays(1, &s.vao);
glBindVertexArray(s.vao);
glGenBuffers(1, &s.vbo);
glBindBuffer(GL_ARRAY_BUFFER, s.vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)), vertices.data(),
GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex),
reinterpret_cast<const void*>(offsetof(Vertex, r)));
const unsigned short indices[] = {0, 1, 2, 3};
glGenBuffers(1, &s.ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s.ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_DYNAMIC_DRAW);
const float white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
glGenBuffers(1, &s.ubo);
glBindBuffer(GL_UNIFORM_BUFFER, s.ubo);
glBufferData(GL_UNIFORM_BUFFER, sizeof(white), white, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, s.ubo);
glUseProgram(s.program);
return s;
}
void Destroy(Scene& s) {
glDeleteBuffers(1, &s.ubo);
glDeleteBuffers(1, &s.ibo);
glDeleteBuffers(1, &s.vbo);
glDeleteVertexArrays(1, &s.vao);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &s.fbo);
glDeleteTextures(1, &s.color);
glDeleteProgram(s.program);
}
} // namespace
TEST_F(WriteRenameScenario, AVertexWriteBetweenDrawsKeepsOrderAndCarriesTheUntouchedBytes) {
if (!Ready()) return;
std::vector<Vertex> v(8);
PutQuad(v, 0, 0.0f, 0.0f, 0.5f, 1.0f, 255, 0, 0); // left half, red
PutQuad(v, 4, 0.5f, 0.0f, 1.0f, 1.0f, 0, 255, 0); // right half, green
std::string error;
Scene s = MakeScene(v, &error);
ASSERT_NE(s.program, 0u) << error;
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // left half red: the bytes before the write
// Rewrite the first quad only: the top-left quarter, blue. The second quad's bytes are
// not part of the write.
std::vector<Vertex> q(4);
PutQuad(q, 0, 0.0f, 0.5f, 0.5f, 1.0f, 0, 0, 255);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(q.size() * sizeof(Vertex)), q.data());
glDrawArrays(GL_TRIANGLE_STRIP, 4, 4); // right half: the untouched second quad
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // top-left quarter: the new first quad
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(At(kSize / 4, kSize / 4), 255, 0, 0)) << "bottom-left: the draw before the write";
EXPECT_TRUE(Near(At(kSize / 4, kSize * 3 / 4), 0, 0, 255)) << "top-left: the draw after the write";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 255, 0)) << "right: bytes the write did not touch";
Destroy(s);
}
TEST_F(WriteRenameScenario, AOneByteWriteBetweenDrawsIsSeenByTheNextDrawOnly) {
if (!Ready()) return;
std::vector<Vertex> v(4);
PutQuad(v, 0, 0.0f, 0.0f, 0.5f, 1.0f, 255, 0, 0); // left half, red
std::string error;
Scene s = MakeScene(v, &error);
ASSERT_NE(s.program, 0u) << error;
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// Move the quad right (eight-byte position writes) and add blue to every vertex with
// one-byte writes, which no four-byte-aligned copy can carry.
std::vector<Vertex> moved(4);
PutQuad(moved, 0, 0.5f, 0.0f, 1.0f, 1.0f, 255, 0, 0);
for (int i = 0; i < 4; ++i)
glBufferSubData(GL_ARRAY_BUFFER, i * sizeof(Vertex), 8, &moved[i]);
for (int i = 0; i < 4; ++i) {
const unsigned char blue = 255;
glBufferSubData(GL_ARRAY_BUFFER, i * sizeof(Vertex) + offsetof(Vertex, b), 1, &blue);
}
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "left: the draw before the writes";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 255, 0, 255)) << "right: red plus the one-byte blue";
Destroy(s);
}
TEST_F(WriteRenameScenario, UniformAndIndexWritesBetweenDrawsAreSeenByTheNextDraw) {
if (!Ready()) return;
std::vector<Vertex> v(8);
PutQuad(v, 0, 0.0f, 0.0f, 0.5f, 1.0f, 255, 255, 255); // left half, white
PutQuad(v, 4, 0.5f, 0.0f, 1.0f, 1.0f, 255, 255, 255); // right half, white
std::string error;
Scene s = MakeScene(v, &error);
ASSERT_NE(s.program, 0u) << error;
const float red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glBindBuffer(GL_UNIFORM_BUFFER, s.ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(red), red);
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr); // left half, red
// Now the tint is green and the indices name the second quad.
const float green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(green), green);
const unsigned short second[] = {4, 5, 6, 7};
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(second), second);
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, nullptr); // right half, green
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "left: tint and indices before the writes";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 255, 0)) << "right: tint and indices after the writes";
Destroy(s);
}
TEST_F(WriteRenameScenario, ARenamedStoreKeepsDrawingRightInLaterFrames) {
if (!Ready()) return;
std::vector<Vertex> v(4);
PutQuad(v, 0, 0.0f, 0.0f, 0.5f, 1.0f, 255, 0, 0);
std::string error;
Scene s = MakeScene(v, &error);
ASSERT_NE(s.program, 0u) << error;
for (int frame = 0; frame < 6; ++frame) {
glBindFramebuffer(GL_FRAMEBUFFER, s.fbo);
glViewport(0, 0, kSize, kSize);
glClear(GL_COLOR_BUFFER_BIT);
// Each frame: draw the left quad, move it right, draw again - one rename per frame.
std::vector<Vertex> left(4), right(4);
PutQuad(left, 0, 0.0f, 0.0f, 0.5f, 1.0f, 255, 0, 0);
PutQuad(right, 0, 0.5f, 0.0f, 1.0f, 1.0f, 0, 0, 255);
glBindBuffer(GL_ARRAY_BUFFER, s.vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(left.size() * sizeof(Vertex)), left.data());
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast<GLsizeiptr>(right.size() * sizeof(Vertex)), right.data());
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "frame " << frame << " left";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 0, 255)) << "frame " << frame << " right";
Gl().EndFrame();
}
EXPECT_EQ(FirstGLError(), 0u);
Destroy(s);
}
} // namespace MGITest