diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 1aa549c6..a65641e9 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 6bdbef33..b68e5bbb 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 4f9ea7a7..1776f8d1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -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(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(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(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(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(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(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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 24b817c5..ebce7b79 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -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 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(res.Gen) << 32) | res.Slot; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 095c53c8..fad0c78b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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(m_wireStreamsConverted), - static_cast(m_wireStreamsReused)); + MGLOG_I("MGWIRE-VCONV[shutdown] converted=%llu reused=%llu renamedWrites=%llu", + static_cast(m_wireStreamsConverted), + static_cast(m_wireStreamsReused), + static_cast(m_bufferManager.GetWireRenameCount())); MGLOG_I("MGWIRE-SPIRV-SCREEN[shutdown] modulesValidated=%llu", static_cast(ProgramFactory::DriverModuleValidatorRuns())); DestroyWireDrawPass(); diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 8b65829e..663acfc9 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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.*" "") diff --git a/MobileGL/MG_IntegrationTest/Scenarios/WriteRenameScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/WriteRenameScenario.cpp new file mode 100644 index 00000000..0629894d --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/WriteRenameScenario.cpp @@ -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 +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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& 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& 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(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(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 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 q(4); + PutQuad(q, 0, 0.0f, 0.5f, 0.5f, 1.0f, 0, 0, 255); + glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast(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 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 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 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 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 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(left.size() * sizeof(Vertex)), left.data()); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBufferSubData(GL_ARRAY_BUFFER, 0, static_cast(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