diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a65641e9..239a4796 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -140,6 +140,10 @@ namespace MobileGL::MG_Config { // 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_ESPRYT_VAO_SHADOW: DirectGLES emits a vertex attribute's enable flag, divisor and + // pointer to the driver VAO only when they differ from what that VAO already holds (a + // shadow on the VAO twin). Auto is ON; ForceOff re-emits the whole configuration per walk. + QuirkOverride EsprytVaoShadow = 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 b68e5bbb..8848e8f8 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -350,6 +350,7 @@ namespace MobileGL::MG_ConfigLoader { features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE"); features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE"); features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME"); + features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW"); 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/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ef2fe908..7309308d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4770,6 +4770,36 @@ namespace MobileGL::MG_Backend::DirectGLES { BindBackendVAOId(m_backendVAOId); } + // P15: the driver-VAO shadow (Managers.h). Callers have bound this twin's VAO. + namespace { + inline Bool VaoShadowOn() { + return MG_Config::Features.EsprytVaoShadow != MG_Config::QuirkOverride::ForceOff; + } + } // namespace + + void BackendVertexArrayObject::HostSetEnabled(Uint index, Bool enabled) { + const Uint32 bit = 1u << index; + if (VaoShadowOn() && (m_hostEnabledKnownMask & bit) && ((m_hostEnabledMask & bit) != 0) == enabled) return; + if (enabled) g_GLESFuncs.glEnableVertexAttribArray(index); + else g_GLESFuncs.glDisableVertexAttribArray(index); + m_hostEnabledKnownMask |= bit; + m_hostEnabledMask = enabled ? (m_hostEnabledMask | bit) : (m_hostEnabledMask & ~bit); + } + + void BackendVertexArrayObject::HostSetDivisor(Uint index, Uint32 divisor) { + const Uint32 bit = 1u << index; + if (VaoShadowOn() && (m_hostDivisorKnownMask & bit) && m_hostDivisor[index] == divisor) return; + g_GLESFuncs.glVertexAttribDivisor(index, divisor); + m_hostDivisorKnownMask |= bit; + m_hostDivisor[index] = divisor; + } + + void BackendVertexArrayObject::HostForgetAll() { + m_hostEnabledKnownMask = 0; + m_hostDivisorKnownMask = 0; + for (auto& pointer : m_hostPointer) pointer.valid = false; + } + // ES 3.1 core. Queried through the loader rather than the version, because the whole // point of using it is to express something the pointer API cannot, and falling back // silently on a driver that lacks it is better than crashing on a null entry point. @@ -4845,23 +4875,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return static_cast(baseInstance) * static_cast(stride); } - // BindAttributeBuffer, resolving the driver id from the slot table instead of from the - // attribute's SharedPtr. It does NOT ensure storage: on this arm the draw's buffers - // were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw, which is - // the one place that still holds the frontend objects (a pull site P4b/P8 own). - inline Bool BindAttributeBufferByHandle(MG_Pipe::MGPipeHandle res) { - if (MG_Pipe::MGPipeHandleIsNull(res)) { - MGLOG_W_ONCE("Attribute has no bound buffer, skipping."); - return false; - } - auto* backendResource = BufferImpl::FindBufferResourceForHandle(res); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute."); - return false; - } - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id); - return true; - } + // (The attribute-buffer bind lives in SyncToBackendFromApplier's pointer path now: it + // resolves the driver id from the slot table and skips the bind and the pointer when the + // driver VAO already holds them. It does NOT ensure storage: on this arm the draw's + // buffers were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw.) // SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved // stride of zero, which glVertexAttribPointer's zero means the opposite of. @@ -5010,6 +5027,10 @@ namespace MobileGL::MG_Backend::DirectGLES { m_hasConvertedFloat64Attribute = false; Bind(); + if (m_hostPointerBufferGeneration != currentBufferIdGeneration) { + for (auto& pointer : m_hostPointer) pointer.valid = false; + m_hostPointerBufferGeneration = currentBufferIdGeneration; + } // ALL 32 SLOTS, not rec->AttributeCount, and the difference is the disable arm. // The legacy walk ran over the frontend's whole 32-slot attribute array and reached @@ -5035,13 +5056,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // compare: the applier's Attributes[] IS what was last pushed, so a moved // ContentSerial means re-emit and an unchanged one means the early-out above // already returned. A buffers-only walk leaves the flags as they are (above). - if (!buffersOnly) { - if (attrib.Enabled) { - g_GLESFuncs.glEnableVertexAttribArray(attribIndex); - } else { - g_GLESFuncs.glDisableVertexAttribArray(attribIndex); - } - } + if (!buffersOnly) HostSetEnabled(attribIndex, attrib.Enabled != 0); // The fp64 narrowing, verbatim in behaviour including the Adreno workaround: // when no float32 stream can be built the array is DISABLED rather than left @@ -5055,10 +5070,13 @@ namespace MobileGL::MG_Backend::DirectGLES { binding != nullptr && SyncFloat64AttributeAsFloat32ByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) { m_hasConvertedFloat64Attribute = true; - // Explicit, not redundant: an earlier walk that could not build the - // stream disabled this array. - g_GLESFuncs.glEnableVertexAttribArray(attribIndex); - g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor); + // The narrowing pointed the attribute at its own stream (and may have + // used the binding API), so the pointer and divisor shadow are stale. + // An earlier walk that could not build the stream disabled this array. + m_hostPointer[attribIndex].valid = false; + m_hostDivisorKnownMask &= ~(1u << attribIndex); + HostSetEnabled(attribIndex, true); + HostSetDivisor(attribIndex, divisor); continue; } if (attrib.Enabled) { @@ -5066,7 +5084,7 @@ namespace MobileGL::MG_Backend::DirectGLES { "stream could not be narrowed to float32 - disabling the array", attribIndex); } - g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + HostSetEnabled(attribIndex, false); continue; } @@ -5081,15 +5099,28 @@ namespace MobileGL::MG_Backend::DirectGLES { // the opposite. ES 3.1's binding-point API can. if (attrib.Stride == 0 && HasVertexBindingApi()) { walkWasPlain = false; + // The binding API rewrote this attribute's format and binding: whatever the + // shadow said about its pointer no longer holds. + m_hostPointer[attribIndex].valid = false; if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) { continue; } - // No shift here on purpose: a zero stride never advances. + // No shift here on purpose: a zero stride never advances. Binding index == + // attribute index here, so this IS the attribute's divisor for the shadow. g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor); + m_hostDivisorKnownMask |= 1u << attribIndex; + m_hostDivisor[attribIndex] = divisor; continue; } - if (!BindAttributeBufferByHandle(binding->Res)) { + if (MG_Pipe::MGPipeHandleIsNull(binding->Res)) { + MGLOG_W_ONCE("Attribute has no bound buffer, skipping."); + walkWasPlain = false; + continue; + } + const auto* attribBuffer = BufferImpl::FindBufferResourceForHandle(binding->Res); + if (!attribBuffer || attribBuffer->id == 0) { + MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute."); walkWasPlain = false; continue; } @@ -5110,15 +5141,33 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT fetchOffset = static_cast(attrib.Offset) + BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)); + const GLint glSize = (!attrib.IsInteger && attrib.IsBgra) ? static_cast(GL_BGRA) + : static_cast(attrib.Size); - if (!attrib.IsInteger) { - const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : static_cast(attrib.Size); - g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType, - attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, - (const void*)fetchOffset); - } else { - g_GLESFuncs.glVertexAttribIPointer(attribIndex, static_cast(attrib.Size), glType, - attrib.Stride, (const void*)fetchOffset); + // The pointer the driver VAO already holds for this attribute - the buffer it + // captured included - needs no glBindBuffer + pointer call. A format that may be + // refused always goes to the driver, since its glGetError is the point. + auto& shadowPointer = m_hostPointer[attribIndex]; + const Bool pointerHeld = VaoShadowOn() && !formatMayBeRefused && shadowPointer.valid && + shadowPointer.bufferId == attribBuffer->id && shadowPointer.size == glSize && + shadowPointer.type == static_cast(glType) && + shadowPointer.stride == static_cast(attrib.Stride) && + shadowPointer.offset == fetchOffset && + shadowPointer.normalized == (attrib.Normalized != 0) && + shadowPointer.integer == (attrib.IsInteger != 0); + if (!pointerHeld) { + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, attribBuffer->id); + if (!attrib.IsInteger) { + g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType, + attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, + (const void*)fetchOffset); + } else { + g_GLESFuncs.glVertexAttribIPointer(attribIndex, glSize, glType, attrib.Stride, + (const void*)fetchOffset); + } + shadowPointer = {attribBuffer->id, glSize, static_cast(glType), + static_cast(attrib.Stride), fetchOffset, attrib.Normalized != 0, + attrib.IsInteger != 0, true}; } if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) { @@ -5127,11 +5176,12 @@ namespace MobileGL::MG_Backend::DirectGLES { "fetch through a pointer the driver never accepted", attribIndex, static_cast(attrib.Size), attrib.IsBgra ? 1 : 0, MG_Util::ConvertGLEnumToString(glType).c_str()); - g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + shadowPointer.valid = false; + HostSetEnabled(attribIndex, false); continue; } - g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor); + HostSetDivisor(attribIndex, divisor); } if (indexBufferDirty) { @@ -5224,6 +5274,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // the next sync must not take its buffers-only walk, nor adopt an equal buffer set. m_lastWalkWasPlain = false; m_syncedBuffersValid = false; + HostForgetAll(); // the uploads below set enables, pointers and divisors directly Bind(); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index cac7a4ca..18758269 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1417,6 +1417,32 @@ namespace MobileGL::MG_Backend::DirectGLES { Array m_syncedBuffers{}; Uint32 m_syncedBufferStart = 0, m_syncedBufferCount = 0, m_syncedBufferBaseInstance = 0; Bool m_syncedBuffersValid = false; + // P15: WHAT THIS TWIN'S DRIVER VAO HOLDS, as the walk last set it. The walk re-emits + // a whole configuration whenever its record moves, and most of it equals what the + // driver VAO already has: on FCL MC 1.21.5, about 2870 of 3700 host calls a frame were + // enable/disable/divisor re-sets. The shadow lets the walk emit only differences. It + // is valid from creation (a new VAO is all-disabled with divisor 0). Anything that + // writes the VAO behind the walk (client-memory uploads, the fp64 narrowing, the + // binding-API zero stride, a refused BGRA format) forgets the affected part. Pointer + // entries name driver buffer ids, so they also go when g_bufferBackendIdGeneration + // moves: an id can be re-minted. + struct HostAttribPointer { + Uint bufferId = 0; + Int32 size = 0; + Uint32 type = 0; + Int32 stride = 0; + SizeT offset = 0; + Bool normalized = false, integer = false, valid = false; + }; + Uint32 m_hostEnabledMask = 0; + Uint32 m_hostEnabledKnownMask = ~0u; + Array m_hostDivisor{}; + Uint32 m_hostDivisorKnownMask = ~0u; + Array m_hostPointer{}; + Uint64 m_hostPointerBufferGeneration = 0; + void HostSetEnabled(Uint index, Bool enabled); + void HostSetDivisor(Uint index, Uint32 divisor); + void HostForgetAll(); MG_Pipe::MGPipeHandle m_syncedIndexRes = MG_Pipe::kMGPipeNullHandle; Bool m_syncedIndexValid = false; Uint64 m_syncedContextSerial = 0; diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 663acfc9..aa0bc129 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -101,6 +101,7 @@ add_executable(MobileGLIntegrationTest # P15 S0: a CPU-converted vertex stream reused and re-written within a frame (DirectVulkan caches it). Scenarios/ConvertedVertexStreamScenario.cpp Scenarios/WriteRenameScenario.cpp + Scenarios/VaoShadowScenario.cpp Scenarios/SampleVariablesScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp Scenarios/DepthStencilReadbackMatrixScenario.cpp @@ -2460,6 +2461,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) mgl_itest_register_split_arms("InPassClearScenario.*" "") mgl_itest_register_split_arms("ConvertedVertexStreamScenario.*" "") mgl_itest_register_split_arms("WriteRenameScenario.*" "") + mgl_itest_register_split_arms("VaoShadowScenario.*" "") # 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/VaoShadowScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/VaoShadowScenario.cpp new file mode 100644 index 00000000..ab475c5b --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/VaoShadowScenario.cpp @@ -0,0 +1,240 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VaoShadowScenario.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 - ONE VAO WHOSE ATTRIBUTE STATE CHANGES BETWEEN DRAWS. +// +// P15: DirectGLES keeps a shadow of what each driver VAO holds (enable flags, divisors, pointers +// with the buffer they captured) and emits only differences. What the shadow must never do is +// skip a change the driver VAO needs: +// +// * an attribute disabled between two draws reads its current value in the second; +// * the same layout pointed at another buffer reads that buffer; +// * a deleted buffer replaced by a new one (its driver id may be re-used) is read anew; +// * a divisor changed between two instanced draws takes effect. +// +// DirectVulkan is the control; MOBILEGL_ESPRYT_VAO_SHADOW=0 re-emits everything. + +#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] (+ an x offset per instance), colour from attribute 1. + 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 + vec2(float(gl_InstanceID) * 0.5, 0.0)) * 2.0 - 1.0, 0.0, 1.0); +} +)"; + + constexpr const char* kFS = R"(#version 330 core +in vec4 vColor; +out vec4 o0; +void main() { o0 = vColor; } +)"; + + constexpr int kSize = 64; + + class VaoShadowScenario : public ScenarioTest {}; + + ::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); } + + // Quad positions (x0..x1 by full height) as a triangle strip. + std::vector Quad(float x0, float x1) { return {x0, 0.0f, x1, 0.0f, x0, 1.0f, x1, 1.0f}; } + std::vector Colors(float r, float g, float b) { + std::vector c; + for (int i = 0; i < 4; ++i) c.insert(c.end(), {r, g, b, 1.0f}); + return c; + } + + struct Scene { + GLuint program = 0, fbo = 0, color = 0, vao = 0; + }; + + Scene MakeScene(std::string* error) { + Scene s; + s.program = CompileProgram(kVS, kFS, error); + if (s.program == 0) return s; + 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_BLEND); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glGenVertexArrays(1, &s.vao); + glBindVertexArray(s.vao); + glUseProgram(s.program); + return s; + } + + GLuint Buffer(const std::vector& data) { + GLuint b = 0; + glGenBuffers(1, &b); + glBindBuffer(GL_ARRAY_BUFFER, b); + glBufferData(GL_ARRAY_BUFFER, static_cast(data.size() * sizeof(float)), data.data(), GL_STATIC_DRAW); + return b; + } + + void Destroy(Scene& s) { + glDeleteVertexArrays(1, &s.vao); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &s.fbo); + glDeleteTextures(1, &s.color); + glDeleteProgram(s.program); + } + + } // namespace + + TEST_F(VaoShadowScenario, AnAttributeDisabledBetweenDrawsReadsItsCurrentValue) { + if (!Ready()) return; + std::string error; + Scene s = MakeScene(&error); + ASSERT_NE(s.program, 0u) << error; + const GLuint left = Buffer(Quad(0.0f, 0.5f)), right = Buffer(Quad(0.5f, 1.0f)); + const GLuint red = Buffer(Colors(1.0f, 0.0f, 0.0f)); + glBindBuffer(GL_ARRAY_BUFFER, red); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, left); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // left: red from the buffer + glDisableVertexAttribArray(1); + glVertexAttrib4f(1, 0.0f, 0.0f, 1.0f, 1.0f); + glBindBuffer(GL_ARRAY_BUFFER, right); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // right: the current value, blue + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "left: attribute 1 enabled"; + EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 0, 255)) << "right: attribute 1 disabled"; + // And back on: the driver VAO must be re-enabled, not left as the shadow last saw it. + glClear(GL_COLOR_BUFFER_BIT); + glEnableVertexAttribArray(1); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 255, 0, 0)) << "right: attribute 1 enabled again"; + GLuint bufs[] = {left, right, red}; + glDeleteBuffers(3, bufs); + Destroy(s); + } + + TEST_F(VaoShadowScenario, TheSameLayoutOnAnotherBufferReadsThatBuffer) { + if (!Ready()) return; + std::string error; + Scene s = MakeScene(&error); + ASSERT_NE(s.program, 0u) << error; + const GLuint pos = Buffer(Quad(0.0f, 1.0f)); + const GLuint red = Buffer(Colors(1.0f, 0.0f, 0.0f)), green = Buffer(Colors(0.0f, 1.0f, 0.0f)); + glBindBuffer(GL_ARRAY_BUFFER, pos); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, red); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_TRUE(Near(At(kSize / 2, kSize / 2), 255, 0, 0)) << "first buffer"; + // Identical pointer parameters, other buffer. + glBindBuffer(GL_ARRAY_BUFFER, green); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(At(kSize / 2, kSize / 2), 0, 255, 0)) << "same layout, second buffer"; + GLuint bufs[] = {pos, red, green}; + glDeleteBuffers(3, bufs); + Destroy(s); + } + + TEST_F(VaoShadowScenario, ADeletedBufferReplacedByANewOneIsReadAnew) { + if (!Ready()) return; + std::string error; + Scene s = MakeScene(&error); + ASSERT_NE(s.program, 0u) << error; + const GLuint pos = Buffer(Quad(0.0f, 1.0f)); + glBindBuffer(GL_ARRAY_BUFFER, pos); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + GLuint first = Buffer(Colors(1.0f, 0.0f, 0.0f)); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_TRUE(Near(At(kSize / 2, kSize / 2), 255, 0, 0)) << "the first buffer"; + // Delete it and make another (the driver may hand the same id back), same parameters. + glDeleteBuffers(1, &first); + const GLuint second = Buffer(Colors(0.0f, 0.0f, 1.0f)); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(At(kSize / 2, kSize / 2), 0, 0, 255)) << "the replacement buffer"; + GLuint bufs[] = {pos, second}; + glDeleteBuffers(2, bufs); + Destroy(s); + } + + TEST_F(VaoShadowScenario, ADivisorChangedBetweenInstancedDrawsTakesEffect) { + if (!Ready()) return; + std::string error; + Scene s = MakeScene(&error); + ASSERT_NE(s.program, 0u) << error; + // Two instances of the left half (instance 1 lands on the right half). + const GLuint pos = Buffer(Quad(0.0f, 0.5f)); + // Per-vertex: 4 x red. Per-instance (divisor 1): instance 0 red, instance 1 green. + std::vector c = Colors(1.0f, 0.0f, 0.0f); + c[4] = 0.0f; c[5] = 1.0f; c[6] = 0.0f; // second element green + const GLuint colors = Buffer(c); + glBindBuffer(GL_ARRAY_BUFFER, pos); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, colors); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); + EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 255, 0)) << "divisor 1: instance 1 reads element 1"; + glClear(GL_COLOR_BUFFER_BIT); + glVertexAttribDivisor(1, 0); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); + EXPECT_EQ(FirstGLError(), 0u); + // Divisor 0: per vertex - only vertex 1 (bottom-right of each quad) is green, so the right + // quad's top-left corner is almost pure red (a divisor still at 1 would make it pure green). + const Rgba8 corner = At(kSize / 2 + 2, kSize - 2); + EXPECT_TRUE(corner.r > 200 && corner.g < 40) + << "divisor 0: the right quad's top-left reads per-vertex red, got (" << int(corner.r) << ", " + << int(corner.g) << ", " << int(corner.b) << ")"; + GLuint bufs[] = {pos, colors}; + glDeleteBuffers(2, bufs); + Destroy(s); + } + +} // namespace MGITest