[Perf] (Espryt): a shadow of each driver VAO's attribute state makes the vertex walk emit only differences (FCL host GLES calls 3717 -> 850/frame, CPU 2.78 -> 2.67 ms at cpuhunt).

This commit is contained in:
2026-10-08 10:49:29 -04:00
parent 6985e47025
commit b4e209e12b
6 changed files with 365 additions and 41 deletions
+4
View File
@@ -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 // 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. // copy that ends the render pass. Auto is ON; ForceOff keeps the staged copy.
QuirkOverride MagmaWriteRename = QuirkOverride::Auto; 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 // 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 // string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension // module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1
View File
@@ -350,6 +350,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE"); features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE"); features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME"); features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64"); features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV"); features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
+92 -41
View File
@@ -4770,6 +4770,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBackendVAOId(m_backendVAOId); 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 // 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 // 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. // 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<SizeT>(baseInstance) * static_cast<SizeT>(stride); return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(stride);
} }
// BindAttributeBuffer, resolving the driver id from the slot table instead of from the // (The attribute-buffer bind lives in SyncToBackendFromApplier's pointer path now: it
// attribute's SharedPtr. It does NOT ensure storage: on this arm the draw's buffers // resolves the driver id from the slot table and skips the bind and the pointer when the
// were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw, which is // driver VAO already holds them. It does NOT ensure storage: on this arm the draw's
// the one place that still holds the frontend objects (a pull site P4b/P8 own). // buffers were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw.)
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;
}
// SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved // SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved
// stride of zero, which glVertexAttribPointer's zero means the opposite of. // stride of zero, which glVertexAttribPointer's zero means the opposite of.
@@ -5010,6 +5027,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_hasConvertedFloat64Attribute = false; m_hasConvertedFloat64Attribute = false;
Bind(); 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. // 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 // 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 // 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 // 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). // already returned. A buffers-only walk leaves the flags as they are (above).
if (!buffersOnly) { if (!buffersOnly) HostSetEnabled(attribIndex, attrib.Enabled != 0);
if (attrib.Enabled) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
}
}
// The fp64 narrowing, verbatim in behaviour including the Adreno workaround: // The fp64 narrowing, verbatim in behaviour including the Adreno workaround:
// when no float32 stream can be built the array is DISABLED rather than left // 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 && binding != nullptr &&
SyncFloat64AttributeAsFloat32ByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) { SyncFloat64AttributeAsFloat32ByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) {
m_hasConvertedFloat64Attribute = true; m_hasConvertedFloat64Attribute = true;
// Explicit, not redundant: an earlier walk that could not build the // The narrowing pointed the attribute at its own stream (and may have
// stream disabled this array. // used the binding API), so the pointer and divisor shadow are stale.
g_GLESFuncs.glEnableVertexAttribArray(attribIndex); // An earlier walk that could not build the stream disabled this array.
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor); m_hostPointer[attribIndex].valid = false;
m_hostDivisorKnownMask &= ~(1u << attribIndex);
HostSetEnabled(attribIndex, true);
HostSetDivisor(attribIndex, divisor);
continue; continue;
} }
if (attrib.Enabled) { if (attrib.Enabled) {
@@ -5066,7 +5084,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
"stream could not be narrowed to float32 - disabling the array", "stream could not be narrowed to float32 - disabling the array",
attribIndex); attribIndex);
} }
g_GLESFuncs.glDisableVertexAttribArray(attribIndex); HostSetEnabled(attribIndex, false);
continue; continue;
} }
@@ -5081,15 +5099,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the opposite. ES 3.1's binding-point API can. // the opposite. ES 3.1's binding-point API can.
if (attrib.Stride == 0 && HasVertexBindingApi()) { if (attrib.Stride == 0 && HasVertexBindingApi()) {
walkWasPlain = false; 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)) { if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) {
continue; 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); g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor);
m_hostDivisorKnownMask |= 1u << attribIndex;
m_hostDivisor[attribIndex] = divisor;
continue; 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; walkWasPlain = false;
continue; continue;
} }
@@ -5110,15 +5141,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) + const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) +
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)); const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type));
const GLint glSize = (!attrib.IsInteger && attrib.IsBgra) ? static_cast<GLint>(GL_BGRA)
: static_cast<GLint>(attrib.Size);
if (!attrib.IsInteger) { // The pointer the driver VAO already holds for this attribute - the buffer it
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(attrib.Size); // captured included - needs no glBindBuffer + pointer call. A format that may be
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType, // refused always goes to the driver, since its glGetError is the point.
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride, auto& shadowPointer = m_hostPointer[attribIndex];
(const void*)fetchOffset); const Bool pointerHeld = VaoShadowOn() && !formatMayBeRefused && shadowPointer.valid &&
} else { shadowPointer.bufferId == attribBuffer->id && shadowPointer.size == glSize &&
g_GLESFuncs.glVertexAttribIPointer(attribIndex, static_cast<GLint>(attrib.Size), glType, shadowPointer.type == static_cast<Uint32>(glType) &&
attrib.Stride, (const void*)fetchOffset); shadowPointer.stride == static_cast<Int32>(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<Uint32>(glType),
static_cast<Int32>(attrib.Stride), fetchOffset, attrib.Normalized != 0,
attrib.IsInteger != 0, true};
} }
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) { 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", "fetch through a pointer the driver never accepted",
attribIndex, static_cast<int>(attrib.Size), attrib.IsBgra ? 1 : 0, attribIndex, static_cast<int>(attrib.Size), attrib.IsBgra ? 1 : 0,
MG_Util::ConvertGLEnumToString(glType).c_str()); MG_Util::ConvertGLEnumToString(glType).c_str());
g_GLESFuncs.glDisableVertexAttribArray(attribIndex); shadowPointer.valid = false;
HostSetEnabled(attribIndex, false);
continue; continue;
} }
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor); HostSetDivisor(attribIndex, divisor);
} }
if (indexBufferDirty) { 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. // the next sync must not take its buffers-only walk, nor adopt an equal buffer set.
m_lastWalkWasPlain = false; m_lastWalkWasPlain = false;
m_syncedBuffersValid = false; m_syncedBuffersValid = false;
HostForgetAll(); // the uploads below set enables, pointers and divisors directly
Bind(); Bind();
+26
View File
@@ -1417,6 +1417,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<MG_Pipe::MGPVertexBuffer, MG_Pipe::kMGPipeMaxVertexAttribs> m_syncedBuffers{}; Array<MG_Pipe::MGPVertexBuffer, MG_Pipe::kMGPipeMaxVertexAttribs> m_syncedBuffers{};
Uint32 m_syncedBufferStart = 0, m_syncedBufferCount = 0, m_syncedBufferBaseInstance = 0; Uint32 m_syncedBufferStart = 0, m_syncedBufferCount = 0, m_syncedBufferBaseInstance = 0;
Bool m_syncedBuffersValid = false; 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<Uint32, MG_Pipe::kMGPipeMaxVertexAttribs> m_hostDivisor{};
Uint32 m_hostDivisorKnownMask = ~0u;
Array<HostAttribPointer, MG_Pipe::kMGPipeMaxVertexAttribs> 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; MG_Pipe::MGPipeHandle m_syncedIndexRes = MG_Pipe::kMGPipeNullHandle;
Bool m_syncedIndexValid = false; Bool m_syncedIndexValid = false;
Uint64 m_syncedContextSerial = 0; Uint64 m_syncedContextSerial = 0;
@@ -101,6 +101,7 @@ add_executable(MobileGLIntegrationTest
# P15 S0: a CPU-converted vertex stream reused and re-written within a frame (DirectVulkan caches it). # P15 S0: a CPU-converted vertex stream reused and re-written within a frame (DirectVulkan caches it).
Scenarios/ConvertedVertexStreamScenario.cpp Scenarios/ConvertedVertexStreamScenario.cpp
Scenarios/WriteRenameScenario.cpp Scenarios/WriteRenameScenario.cpp
Scenarios/VaoShadowScenario.cpp
Scenarios/SampleVariablesScenario.cpp Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp Scenarios/DepthStencilReadbackMatrixScenario.cpp
@@ -2460,6 +2461,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_register_split_arms("InPassClearScenario.*" "") mgl_itest_register_split_arms("InPassClearScenario.*" "")
mgl_itest_register_split_arms("ConvertedVertexStreamScenario.*" "") mgl_itest_register_split_arms("ConvertedVertexStreamScenario.*" "")
mgl_itest_register_split_arms("WriteRenameScenario.*" "") 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 # 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. # core-created ones, so the scenario runs on every split arm as well as both monoliths.
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "") mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
@@ -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 <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] (+ 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<float> Quad(float x0, float x1) { return {x0, 0.0f, x1, 0.0f, x0, 1.0f, x1, 1.0f}; }
std::vector<float> Colors(float r, float g, float b) {
std::vector<float> 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<float>& data) {
GLuint b = 0;
glGenBuffers(1, &b);
glBindBuffer(GL_ARRAY_BUFFER, b);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(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<float> 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