mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<SizeT>(baseInstance) * static_cast<SizeT>(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<SizeT>(attrib.Offset) +
|
||||
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
|
||||
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) {
|
||||
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(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<GLint>(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<Uint32>(glType) &&
|
||||
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) {
|
||||
@@ -5127,11 +5176,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"fetch through a pointer the driver never accepted",
|
||||
attribIndex, static_cast<int>(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();
|
||||
|
||||
|
||||
@@ -1417,6 +1417,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Array<MG_Pipe::MGPVertexBuffer, MG_Pipe::kMGPipeMaxVertexAttribs> 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<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;
|
||||
Bool m_syncedIndexValid = false;
|
||||
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).
|
||||
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.*" "")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user