diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 81afb2cd..a86b5d9d 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -53,6 +53,7 @@ add_executable(MobileGLIntegrationTest Harness/BackendCapsPeek.cpp Harness/PipeSlotPeek.cpp Harness/PipeApplyPeek.cpp + Harness/IntegerAttributeTypePeek.cpp Harness/P4aSeamPeek.cpp Harness/P4aFinalFixPeek.cpp Harness/PersistentMapPeek.cpp diff --git a/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.cpp b/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.cpp new file mode 100644 index 00000000..8aa7997a --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.cpp @@ -0,0 +1,279 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.cpp +// Copyright (c) 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 + +#include "IntegerAttributeTypePeek.h" + +#include +#include +#include + +#if !defined(__ANDROID__) +#include +#include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#endif +#define MGITEST_INTEGER_ATTRIBUTE_TYPE_PEEK_LIVE 1 +#endif + +namespace MGITest { + + bool IntegerAttributeTypeFitsInput(unsigned inputType, bool arrayInteger, unsigned arrayType) { + switch (inputType) { + case 0x1404: // GL_INT + case 0x8B53: // GL_INT_VEC2 + case 0x8B54: // GL_INT_VEC3 + case 0x8B55: // GL_INT_VEC4 + return arrayInteger && (arrayType == 0x1400 || arrayType == 0x1402 || arrayType == 0x1404); + case 0x1405: // GL_UNSIGNED_INT + case 0x8DC6: // GL_UNSIGNED_INT_VEC2 + case 0x8DC7: // GL_UNSIGNED_INT_VEC3 + case 0x8DC8: // GL_UNSIGNED_INT_VEC4 + return arrayInteger && (arrayType == 0x1401 || arrayType == 0x1403 || arrayType == 0x1405); + default: + return !arrayInteger; + } + } + +#if defined(MGITEST_INTEGER_ATTRIBUTE_TYPE_PEEK_LIVE) + namespace { + namespace MGB = MobileGL::MG_Backend::DirectGLES; + + // Written and read only on the thread that issues Espryt's driver calls: the test thread + // in monolith, the apply thread under the in-process transport (every access there goes + // through a probe on that thread). + IntegerAttributeTypeTally g_tally; + bool g_watching = false; + + void NoteViolation(GLint program, const char* name, GLint location, GLenum inputType, GLint arrayInteger, + GLint arrayType) { + ++g_tally.violations; + if (!g_tally.firstViolation.empty()) return; + char text[256]; + std::snprintf(text, sizeof(text), + "draw %u: driver program %d input '%s' (type 0x%04x) at location %d reads an array " + "declared %s with type 0x%04x", + g_tally.draws, static_cast(program), name, static_cast(inputType), + static_cast(location), arrayInteger ? "integer" : "float", + static_cast(arrayType)); + g_tally.firstViolation = text; + } + + // Every enabled array an active input of the current driver program reads. + void InspectDraw() { + ++g_tally.draws; + auto& gl = MGB::g_GLESFuncs; + GLint program = 0; + gl.glGetIntegerv(GL_CURRENT_PROGRAM, &program); + if (program == 0) return; + GLint count = 0; + GLint maxLength = 0; + gl.glGetProgramiv(static_cast(program), GL_ACTIVE_ATTRIBUTES, &count); + gl.glGetProgramiv(static_cast(program), GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &maxLength); + std::vector name(static_cast(maxLength > 0 ? maxLength : 0) + 1, '\0'); + bool violated = false; + for (GLint index = 0; index < count && !violated; ++index) { + GLint size = 0; + GLenum type = 0; + gl.glGetActiveAttrib(static_cast(program), static_cast(index), + static_cast(name.size()), nullptr, &size, &type, name.data()); + if (std::strncmp(name.data(), "gl_", 3) == 0) continue; + const GLint location = gl.glGetAttribLocation(static_cast(program), name.data()); + if (location < 0) continue; + GLint enabled = 0; + GLint arrayInteger = 0; + GLint arrayType = 0; + gl.glGetVertexAttribiv(static_cast(location), GL_VERTEX_ATTRIB_ARRAY_ENABLED, &enabled); + if (!enabled) continue; + gl.glGetVertexAttribiv(static_cast(location), GL_VERTEX_ATTRIB_ARRAY_INTEGER, &arrayInteger); + gl.glGetVertexAttribiv(static_cast(location), GL_VERTEX_ATTRIB_ARRAY_TYPE, &arrayType); + if (IntegerAttributeTypeFitsInput(type, arrayInteger != 0, static_cast(arrayType))) continue; + NoteViolation(program, name.data(), location, type, arrayInteger, arrayType); + violated = true; + } + } + + // One hook per driver entry point: the original is kept beside it and called after the + // inspection, with the arguments untouched. +#define MGITEST_HOOK(name, before) \ + struct Hook_##name { \ + static inline decltype(MGB::g_GLESFuncs.name) original = nullptr; \ + template \ + static void Call(Args... args) { \ + before(); \ + original(args...); \ + } \ + static void Install() { \ + if (original != nullptr || MGB::g_GLESFuncs.name == nullptr) return; \ + original = MGB::g_GLESFuncs.name; \ + MGB::g_GLESFuncs.name = &Call; \ + } \ + static void Remove() { \ + if (original == nullptr) return; \ + MGB::g_GLESFuncs.name = original; \ + original = nullptr; \ + } \ + }; + + void CountLink() { + ++g_tally.links; + } + + MGITEST_HOOK(glDrawArrays, InspectDraw) + MGITEST_HOOK(glDrawElements, InspectDraw) + MGITEST_HOOK(glDrawRangeElements, InspectDraw) + MGITEST_HOOK(glDrawArraysInstanced, InspectDraw) + MGITEST_HOOK(glDrawElementsInstanced, InspectDraw) + MGITEST_HOOK(glDrawArraysIndirect, InspectDraw) + MGITEST_HOOK(glDrawElementsIndirect, InspectDraw) + MGITEST_HOOK(glDrawElementsBaseVertex, InspectDraw) + MGITEST_HOOK(glDrawRangeElementsBaseVertex, InspectDraw) + MGITEST_HOOK(glDrawElementsInstancedBaseVertex, InspectDraw) + MGITEST_HOOK(glMultiDrawArraysIndirectEXT, InspectDraw) + MGITEST_HOOK(glMultiDrawElementsIndirectEXT, InspectDraw) + MGITEST_HOOK(glMultiDrawElementsBaseVertexEXT, InspectDraw) + MGITEST_HOOK(glDrawArraysInstancedBaseInstanceEXT, InspectDraw) + MGITEST_HOOK(glDrawElementsInstancedBaseInstanceEXT, InspectDraw) + MGITEST_HOOK(glDrawElementsInstancedBaseVertexBaseInstanceEXT, InspectDraw) + MGITEST_HOOK(glLinkProgram, CountLink) +#undef MGITEST_HOOK + +#define MGITEST_EACH_HOOK(action) \ + Hook_glDrawArrays::action(); \ + Hook_glDrawElements::action(); \ + Hook_glDrawRangeElements::action(); \ + Hook_glDrawArraysInstanced::action(); \ + Hook_glDrawElementsInstanced::action(); \ + Hook_glDrawArraysIndirect::action(); \ + Hook_glDrawElementsIndirect::action(); \ + Hook_glDrawElementsBaseVertex::action(); \ + Hook_glDrawRangeElementsBaseVertex::action(); \ + Hook_glDrawElementsInstancedBaseVertex::action(); \ + Hook_glMultiDrawArraysIndirectEXT::action(); \ + Hook_glMultiDrawElementsIndirectEXT::action(); \ + Hook_glMultiDrawElementsBaseVertexEXT::action(); \ + Hook_glDrawArraysInstancedBaseInstanceEXT::action(); \ + Hook_glDrawElementsInstancedBaseInstanceEXT::action(); \ + Hook_glDrawElementsInstancedBaseVertexBaseInstanceEXT::action(); \ + Hook_glLinkProgram::action(); + + void StartOnDriverThread() { + g_tally = {}; + if (!g_watching) { + MGITEST_EACH_HOOK(Install) + } + g_watching = true; + } + + void StopOnDriverThread() { + if (g_watching) { + MGITEST_EACH_HOOK(Remove) + } + g_watching = false; + } +#undef MGITEST_EACH_HOOK + + // "Is Espryt the backend running": on any other backend no ES entry point was resolved. + bool EsprytIsRunning() { + return MGB::g_GLESFuncs.glDrawArrays != nullptr || Hook_glDrawArrays::original != nullptr; + } + +#if MOBILEGL_BUILD_DISAGGREGATED + bool InProcess() { + return MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::InProcess; + } + + // Runs `body` on the apply thread once every record published so far has been applied. + template + bool OnApplyThread(Body body) { + auto* session = MobileGL::MG_Remote::Client::ClientSession::Active(); + if (session == nullptr) return false; + if (session->WaitForApplied(session->LastPublishedSeq(), 30000) != + MobileGL::MG_Remote::Transport::SessionWait::Reached) + return false; + const auto result = MobileGL::MG_Remote::Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting( + +[](void* opaque) -> MobileGLResult { + (*static_cast(opaque))(); + return MOBILEGL_OK; + }, + &body); + return result == MOBILEGL_OK; + } +#endif + } // namespace + + bool StartIntegerAttributeTypeWatch(std::string* whyNot) { + std::string reason; + bool started = false; + if (MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::Monolith) { + if (EsprytIsRunning()) { + StartOnDriverThread(); + started = true; + } else { + reason = "the backend running is not DirectGLES"; + } + } +#if MOBILEGL_BUILD_DISAGGREGATED + else if (InProcess()) { + bool espryt = false; + const bool reached = OnApplyThread([&]() { + espryt = EsprytIsRunning(); + if (espryt) StartOnDriverThread(); + }); + started = reached && espryt; + if (!started) + reason = reached ? "the backend running is not DirectGLES" : "the apply thread did not answer"; + } +#endif + else { + reason = "the server runs in another process"; + } + if (!started && whyNot != nullptr) *whyNot = reason; + return started; + } + + bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally* out) { + if (out == nullptr) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + if (InProcess()) { + bool watching = false; + if (!OnApplyThread([&]() { + watching = g_watching; + *out = g_tally; + })) + return false; + return watching; + } +#endif + if (MobileGL::MG_Config::Transport != MobileGL::MG_Config::TransportMode::Monolith || !g_watching) return false; + *out = g_tally; + return true; + } + + void StopIntegerAttributeTypeWatch() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (InProcess()) { + OnApplyThread([]() { StopOnDriverThread(); }); + return; + } +#endif + if (MobileGL::MG_Config::Transport == MobileGL::MG_Config::TransportMode::Monolith) StopOnDriverThread(); + } +#else + bool StartIntegerAttributeTypeWatch(std::string* whyNot) { + if (whyNot != nullptr) *whyNot = "Espryt's driver table is not reachable in this build"; + return false; + } + bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally*) { + return false; + } + void StopIntegerAttributeTypeWatch() {} +#endif + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.h b/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.h new file mode 100644 index 00000000..83f7a08b --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.h @@ -0,0 +1,49 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/IntegerAttributeTypePeek.h +// Copyright (c) 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 +// +// What Espryt hands the ES driver for each integer vertex input, checked at every draw the driver +// receives. ES defines an integer attribute's value only when the array's type has the input's +// signedness (BYTE/SHORT/INT for int, the unsigned three for uint) and a float input only from a +// float array; anything else is undefined, and a driver that sign-extends GL_UNSIGNED_BYTE data +// into an ivec4 input is within its rights. Values alone cannot show this on a driver that happens +// to zero-extend, so the watch reads the driver's own vertex array state at each draw. +// +// A SEPARATE TRANSLATION UNIT for PipeApplyPeek.h's reason: it includes Espryt's own headers, +// which may not meet a scenario's GL headers. Every entry point returns false where it cannot +// look - Android, a backend other than DirectGLES, and a server in another process - and a caller +// that gets false has learned nothing. + +#pragma once + +#include + +namespace MGITest { + + struct IntegerAttributeTypeTally { + // Draws the driver received while the watch was on, and how many of them had at least one + // enabled array whose type the program's input cannot read with a defined value. + unsigned draws = 0; + unsigned violations = 0; + // glLinkProgram calls the driver received, for the cases that bound relinks. + unsigned links = 0; + // The first violation, spelled out for the failure message. + std::string firstViolation; + }; + + // Starts a fresh tally. False where the watch cannot run; `whyNot` then names the reason. + bool StartIntegerAttributeTypeWatch(std::string* whyNot); + // The tally so far, once every draw issued before the call has reached the driver. + bool ReadIntegerAttributeTypeWatch(IntegerAttributeTypeTally* out); + // Puts the driver's entry points back. Harmless when no watch is running. + void StopIntegerAttributeTypeWatch(); + + // The rule the watch applies to one enabled array: whether an input of GL type `inputType` + // reads an array declared (`arrayInteger`, `arrayType`) with a value ES defines. + bool IntegerAttributeTypeFitsInput(unsigned inputType, bool arrayInteger, unsigned arrayType); + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/NarrowIntegerAttributeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/NarrowIntegerAttributeScenario.cpp index dbe22165..e2e3a0c4 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/NarrowIntegerAttributeScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/NarrowIntegerAttributeScenario.cpp @@ -21,13 +21,23 @@ // data, so a pipeline or program cached for one array, or the variant the previous draw settled // on, must never be handed to the other. +// On DirectGLES every case also watches the driver itself (IntegerAttributeTypePeek.h): an array +// whose type has the other signedness from its input reads a value ES leaves undefined, so a case +// can show the right values on one driver and still render black on another. Where the watch +// cannot look it says so and the value checks still run. + #include +#include #include +#include +#include #include #include #include "../Harness/HeadlessGL.h" +#include "../Harness/IntegerAttributeTypePeek.h" #include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" #ifdef GLAPI #undef GLAPI @@ -67,12 +77,14 @@ void main() } )"; - // A capture program: the vertex stage's flat outputs `varyings`, interleaved. - GLuint BuildCaptureProgram(const char* vertexSource, const std::vector& varyings, - std::string* log) { + // A program of `vertexSource` and `fragmentSource` capturing the vertex stage's flat outputs + // `varyings` (none: no capture), interleaved; `beforeLink` runs on it just before the link. + GLuint BuildProgram(const char* vertexSource, const char* fragmentSource, + const std::vector& varyings, std::string* log, + const std::function& beforeLink = {}) { const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log); if (vertexShader == 0) return 0; - const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, kFragmentSource, log); + const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log); if (fragmentShader == 0) { glDeleteShader(vertexShader); return 0; @@ -80,8 +92,11 @@ void main() const GLuint program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); - glTransformFeedbackVaryings(program, static_cast(varyings.size()), varyings.data(), - GL_INTERLEAVED_ATTRIBS); + if (!varyings.empty()) { + glTransformFeedbackVaryings(program, static_cast(varyings.size()), varyings.data(), + GL_INTERLEAVED_ATTRIBS); + } + if (beforeLink) beforeLink(program); glLinkProgram(program); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); @@ -99,6 +114,12 @@ void main() return program; } + // A capture program: the vertex stage's flat outputs `varyings`, interleaved. + GLuint BuildCaptureProgram(const char* vertexSource, const std::vector& varyings, std::string* log, + const std::function& beforeLink = {}) { + return BuildProgram(vertexSource, kFragmentSource, varyings, log, beforeLink); + } + // Every pairing the correction covers, plus two that must keep their value untouched: // matching signedness (location 2) and 32-bit data (location 6). const char* const kEveryPairingVertexSource = R"(#version 410 core @@ -133,6 +154,88 @@ void main() } )"; + const char* const kSingleUintInputVertexSource = R"(#version 410 core +layout(location = 0) in uvec4 value; +flat out uvec4 captured; +void main() +{ + captured = value; + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + + // No layout qualifier: the location comes from glBindAttribLocation. + const char* const kUnplacedInputVertexSource = R"(#version 410 core +in ivec4 value; +flat out ivec4 captured; +void main() +{ + captured = value; + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + + const char* const kWideAndMatchedVertexSource = R"(#version 410 core +layout(location = 0) in ivec2 uintAsInt; +layout(location = 1) in uvec2 ushortAsUint; +flat out ivec4 captured; +void main() +{ + captured = ivec4(uintAsInt, ivec2(ushortAsUint)); + gl_Position = vec4(0.0, 0.0, 0.0, 1.0); +} +)"; + + // A program without a capture list, read back through a 2x1 RGBA32I target instead, so its + // first build is not one that captures. Vertex i lands on pixel i. + const char* const kRenderVertexSource = R"(#version 410 core +layout(location = 0) in ivec4 value; +flat out ivec4 passed; +void main() +{ + passed = value; + gl_Position = vec4(gl_VertexID == 0 ? -0.5 : 0.5, 0.0, 0.0, 1.0); +} +)"; + + const char* const kRenderFragmentSource = R"(#version 410 core +flat in ivec4 passed; +layout(location = 0) out ivec4 color; +void main() +{ + color = passed; +} +)"; + + const char* const kSeparableVertexSource = R"(#version 410 core +layout(location = 0) in ivec4 value; +layout(location = 0) flat out ivec4 passed; +out gl_PerVertex { vec4 gl_Position; }; +void main() +{ + passed = value; + gl_Position = vec4(gl_VertexID == 0 ? -0.5 : 0.5, 0.0, 0.0, 1.0); +} +)"; + + const char* const kSeparableFragmentSource = R"(#version 410 core +layout(location = 0) flat in ivec4 passed; +layout(location = 0) out ivec4 color; +void main() +{ + color = passed; +} +)"; + + // The same four bytes as GL_BYTE and as GL_UNSIGNED_BYTE, and the words each reaches an + // input with: sign-extended from GL_BYTE, zero-extended from GL_UNSIGNED_BYTE, whatever the + // input's own signedness. + const GLubyte kBytes[4] = {0xFF, 0x80, 5, 127}; + const std::vector kBytesAsSigned = {0xFFFFFFFFu, 0xFFFFFF80u, 5u, 127u}; + const std::vector kBytesAsUnsigned = {255u, 128u, 5u, 127u}; + const GLubyte kLightBytes[4] = {240, 200, 128, 7}; + const std::vector kLightWords = {240u, 200u, 128u, 7u}; + // One vertex of kEveryPairingVertexSource's per-vertex data, tightly packed. struct PerVertex { GLubyte ubyteAsInt[4]; @@ -153,10 +256,20 @@ void main() glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureWords * sizeof(GLuint), nullptr, GL_DYNAMIC_READ); glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_capture); ASSERT_EQ(glGetError(), GL_NO_ERROR) << "capture buffer setup"; + std::string whyNot; + m_watching = StartIntegerAttributeTypeWatch(&whyNot); + if (!m_watching) { + // A log line and a property, never a skip: the value checks below still run. + std::cout << "[itest] integer attribute type watch off: " << whyNot << std::endl; + RecordProperty("integer_attribute_type_watch", whyNot); + } } void TearDown() override { if (!Ready()) return; + if (m_watching) StopIntegerAttributeTypeWatch(); + if (m_target != 0) glDeleteFramebuffers(1, &m_target); + if (m_targetTexture != 0) glDeleteTextures(1, &m_targetTexture); glDisable(GL_RASTERIZER_DISCARD); glUseProgram(0); glBindVertexArray(0); @@ -188,7 +301,8 @@ void main() } // One point drawn under GL_RASTERIZER_DISCARD; the first `words` captured words. - std::vector Capture(GLuint program, GLuint vao, std::size_t words) { + std::vector Capture(GLuint program, GLuint vao, std::size_t words, + const std::function& draw = {}) { const std::vector poison(kCaptureWords, kPoison); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_capture); glBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureWords * sizeof(GLuint), poison.data()); @@ -196,7 +310,10 @@ void main() glBindVertexArray(vao); glEnable(GL_RASTERIZER_DISCARD); glBeginTransformFeedback(GL_POINTS); - glDrawArraysInstanced(GL_POINTS, 0, 1, 1); + if (draw) + draw(); + else + glDrawArraysInstanced(GL_POINTS, 0, 1, 1); glEndTransformFeedback(); glDisable(GL_RASTERIZER_DISCARD); std::vector result(words, kPoison); @@ -211,7 +328,74 @@ void main() return result; } + // `points` points drawn into a 2x1 RGBA32I target with `program` (0: the bound pipeline), + // by `draw` or one glDrawArrays; the four words its fragment stage wrote for each. + std::vector Render(GLuint program, GLuint vao, int points = 1, + const std::function& draw = {}) { + if (m_target == 0) { + glGenTextures(1, &m_targetTexture); + glBindTexture(GL_TEXTURE_2D, m_targetTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 1); + glBindTexture(GL_TEXTURE_2D, 0); + glGenFramebuffers(1, &m_target); + glBindFramebuffer(GL_FRAMEBUFFER, m_target); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_targetTexture, 0); + EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + } + glBindFramebuffer(GL_FRAMEBUFFER, m_target); + glViewport(0, 0, 2, 1); + const GLint poison[4] = {static_cast(kPoison), static_cast(kPoison), + static_cast(kPoison), static_cast(kPoison)}; + glClearBufferiv(GL_COLOR, 0, poison); + glUseProgram(program); + glBindVertexArray(vao); + if (draw) + draw(); + else + glDrawArrays(GL_POINTS, 0, 1); + GLint pixels[8] = {}; + glReadPixels(0, 0, 2, 1, GL_RGBA_INTEGER, GL_INT, pixels); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return std::vector(pixels, pixels + 4 * points); + } + + // A vertex array whose location 0 reads `bytes` as four `type` components. + GLuint MakeByteArray(const GLubyte (&bytes)[4], GLenum type) { + const GLuint vao = MakeVertexArray(); + MakeBuffer(bytes, sizeof(bytes)); + glVertexAttribIPointer(0, 4, type, 0, nullptr); + glEnableVertexAttribArray(0); + return vao; + } + + // The driver's view of every draw so far; null where the watch is off. + bool ReadWatch(IntegerAttributeTypeTally* tally) { + if (!m_watching) return false; + glFinish(); + const bool read = ReadIntegerAttributeTypeWatch(tally); + EXPECT_TRUE(read) << "the integer attribute type watch started but could not be read"; + return read; + } + + // Every array a draw so far handed the driver has the signedness of the input it feeds. + void ExpectDriverArraysFitTheirInputs() { + IntegerAttributeTypeTally tally; + if (!ReadWatch(&tally)) return; + RecordProperty("integer_attribute_type_watch_draws", static_cast(tally.draws)); + EXPECT_GT(tally.draws, 0u) << "the watch saw no draw, so it checked nothing"; + EXPECT_EQ(tally.violations, 0u) << tally.firstViolation; + } + + // Driver links so far, or -1 where the watch is off. + long LinksSoFar() { + IntegerAttributeTypeTally tally; + return ReadWatch(&tally) ? static_cast(tally.links) : -1; + } + static constexpr std::size_t kCaptureWords = 16; + bool m_watching = false; + GLuint m_target = 0; + GLuint m_targetTexture = 0; GLuint m_capture = 0; std::vector m_programs; std::vector m_vaos; @@ -258,6 +442,7 @@ void main() }; EXPECT_EQ(captured, expected); EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); } TEST_F(NarrowIntegerAttributeScenario, OneProgramFollowsTheSignednessOfEachVertexArray) { @@ -288,6 +473,350 @@ void main() EXPECT_EQ(Capture(program, signedArray, 4), asSigned) << "GL_BYTE again"; EXPECT_EQ(Capture(program, unsignedArray, 4), asUnsigned) << "GL_UNSIGNED_BYTE again"; EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // The negative control for the driver watch, on canned input: the rule must call an + // unsigned array into an int input wrong, and every matching pairing right. + TEST_F(NarrowIntegerAttributeScenario, TheDriverWatchRuleRejectsAnArrayOfTheOtherSignedness) { + EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, true, GL_UNSIGNED_BYTE)); + EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC2, true, GL_SHORT)); + EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT, true, GL_UNSIGNED_INT)); + EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, false, GL_FLOAT)); + EXPECT_FALSE(IntegerAttributeTypeFitsInput(GL_FLOAT_VEC4, true, GL_BYTE)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT_VEC4, true, GL_BYTE)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT_VEC2, true, GL_SHORT)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_INT, true, GL_INT)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC4, true, GL_UNSIGNED_BYTE)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT_VEC3, true, GL_UNSIGNED_SHORT)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_UNSIGNED_INT, true, GL_UNSIGNED_INT)); + EXPECT_TRUE(IntegerAttributeTypeFitsInput(GL_FLOAT_VEC4, false, GL_UNSIGNED_BYTE)); + } + + // One array read in turn by an int and a uint program: each switch changes which of the two + // signednesses the array has to be issued with, and the second round must need no relink. + TEST_F(NarrowIntegerAttributeScenario, AnIntAndAUintProgramTakeTurnsOnOneArray) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint intProgram = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(intProgram, 0u) << log; + m_programs.push_back(intProgram); + const GLuint uintProgram = BuildCaptureProgram(kSingleUintInputVertexSource, {"captured"}, &log); + ASSERT_NE(uintProgram, 0u) << log; + m_programs.push_back(uintProgram); + const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(intProgram, signedArray, 4), kBytesAsSigned) << "int first"; + EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "then uint"; + const long linksAfterFirstRound = LinksSoFar(); + for (int round = 0; round < 2; ++round) { + EXPECT_EQ(Capture(intProgram, signedArray, 4), kBytesAsSigned) << "int, round " << round; + EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "uint, round " << round; + } + EXPECT_EQ(LinksSoFar(), linksAfterFirstRound) << "switching programs relinked one"; + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // A program that never captures is built for the data it first meets. Fed data of the other + // signedness afterwards it may be rebuilt once, and every value - before, during and after + // the switch - is still GL's. + TEST_F(NarrowIntegerAttributeScenario, AProgramFirstDrawnWithMatchingDataKeepsEveryValue) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE); + const GLuint unsignedArray = MakeByteArray(kBytes, GL_UNSIGNED_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Render(program, signedArray), kBytesAsSigned) << "matching data first"; + const long linksAfterFirstDraw = LinksSoFar(); + EXPECT_EQ(Render(program, unsignedArray), kBytesAsUnsigned) << "then the other signedness"; + EXPECT_EQ(Render(program, signedArray), kBytesAsSigned) << "matching data again"; + EXPECT_EQ(Render(program, unsignedArray), kBytesAsUnsigned) << "the other signedness again"; + if (linksAfterFirstDraw >= 0) EXPECT_LE(LinksSoFar(), linksAfterFirstDraw + 1) << "rebuilt more than once"; + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // Two programs from one source, the first built for matching data only and the second for + // data of the other signedness. Whatever the backend shares between the two builds must not + // hand the second one the first one's shader. + TEST_F(NarrowIntegerAttributeScenario, TwoProgramsFromOneSourceKeepTheirOwnBuilds) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint first = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log); + ASSERT_NE(first, 0u) << log; + m_programs.push_back(first); + const GLuint second = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log); + ASSERT_NE(second, 0u) << log; + m_programs.push_back(second); + const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE); + const GLuint unsignedArray = MakeByteArray(kBytes, GL_UNSIGNED_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Render(first, signedArray), kBytesAsSigned) << "the first program, matching data"; + EXPECT_EQ(Render(second, unsignedArray), kBytesAsUnsigned) << "the second, the other signedness"; + EXPECT_EQ(Render(first, signedArray), kBytesAsSigned) << "the first again"; + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // A relink between two draws of the same data: whatever per-program state the correction + // keeps has to survive the program being rebuilt underneath it. + TEST_F(NarrowIntegerAttributeScenario, ARelinkBetweenDrawsKeepsTheValues) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "before the relink"; + glLinkProgram(program); + GLint status = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &status); + ASSERT_EQ(status, GL_TRUE); + EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "after the relink"; + EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords) << "and once more"; + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // A program and a vertex array deleted and created again, most likely under the same names, + // with the signedness of input and data swapped: nothing learned about the first pair may + // be believed for the second. + TEST_F(NarrowIntegerAttributeScenario, AProgramAndAnArrayCreatedAgainStartOver) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint intProgram = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(intProgram, 0u) << log; + GLuint unsignedArray = 0; + glGenVertexArrays(1, &unsignedArray); + glBindVertexArray(unsignedArray); + MakeBuffer(kLightBytes, sizeof(kLightBytes)); + glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, nullptr); + glEnableVertexAttribArray(0); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "first pair setup"; + EXPECT_EQ(Capture(intProgram, unsignedArray, 4), kLightWords) << "the first pair"; + glUseProgram(0); + glBindVertexArray(0); + glDeleteProgram(intProgram); + glDeleteVertexArrays(1, &unsignedArray); + + const GLuint uintProgram = BuildCaptureProgram(kSingleUintInputVertexSource, {"captured"}, &log); + ASSERT_NE(uintProgram, 0u) << log; + m_programs.push_back(uintProgram); + const GLuint signedArray = MakeByteArray(kBytes, GL_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "second pair setup"; + RecordProperty("program_name_reused", uintProgram == intProgram ? 1 : 0); + RecordProperty("vertex_array_name_reused", signedArray == unsignedArray ? 1 : 0); + EXPECT_EQ(Capture(uintProgram, signedArray, 4), kBytesAsSigned) << "the second pair"; + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // The input's location comes from glBindAttribLocation rather than the shader. + TEST_F(NarrowIntegerAttributeScenario, ABoundAttributeLocationIsCorrected) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kUnplacedInputVertexSource, {"captured"}, &log, + [](GLuint p) { glBindAttribLocation(p, 5, "value"); }); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + ASSERT_EQ(glGetAttribLocation(program, "value"), 5); + const GLuint vao = MakeVertexArray(); + MakeBuffer(kLightBytes, sizeof(kLightBytes)); + glVertexAttribIPointer(5, 4, GL_UNSIGNED_BYTE, 0, nullptr); + glEnableVertexAttribArray(5); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(program, vao, 4), kLightWords); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // The array is described through the direct-state-access entry points. + TEST_F(NarrowIntegerAttributeScenario, ADirectStateAccessArrayIsCorrected) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + GLuint vao = 0; + glCreateVertexArrays(1, &vao); + m_vaos.push_back(vao); + GLuint buffer = 0; + glCreateBuffers(1, &buffer); + m_buffers.push_back(buffer); + glNamedBufferData(buffer, sizeof(kLightBytes), kLightBytes, GL_STATIC_DRAW); + glVertexArrayVertexBuffer(vao, 0, buffer, 0, 4); + glVertexArrayAttribIFormat(vao, 0, 4, GL_UNSIGNED_BYTE, 0); + glVertexArrayAttribBinding(vao, 0, 0); + glEnableVertexArrayAttrib(vao, 0); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(program, vao, 4), kLightWords); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // A binding of stride zero, which never advances: two points read the same element. + TEST_F(NarrowIntegerAttributeScenario, AZeroStrideBindingIsCorrected) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint vao = MakeVertexArray(); + const GLuint buffer = MakeBuffer(kLightBytes, sizeof(kLightBytes)); + glVertexAttribIFormat(0, 4, GL_UNSIGNED_BYTE, 0); + glVertexAttribBinding(0, 0); + glBindVertexBuffer(0, buffer, 0, 0); + glEnableVertexAttribArray(0); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + std::vector expected = kLightWords; + expected.insert(expected.end(), kLightWords.begin(), kLightWords.end()); + EXPECT_EQ(Capture(program, vao, 8, [] { glDrawArrays(GL_POINTS, 0, 2); }), expected); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // An array in the application's own memory. Only a library-hosted backend reads client + // memory at the draw, and only relaxed semantics allow such an array, so the case stops + // before its first draw anywhere else. + TEST_F(NarrowIntegerAttributeScenario, AClientMemoryArrayIsCorrected) { + if (!Ready()) GTEST_SKIP(); + const std::string transport = SplitLane::MarkerValue("MOBILEGL_TRANSPORT"); + if (PeekSplitRuntime().transportResolved || (!transport.empty() && transport != "monolith")) + GTEST_SKIP() << "client-memory arrays are drawn only with the backend in this library"; + GLint profile = 0; + glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &profile); + if ((profile & GL_CONTEXT_CORE_PROFILE_BIT) != 0 && + AmbientQuirkFromEnvironment("MOBILEGL_RELAXED_SEMANTICS") != AmbientQuirk::On) { + GTEST_SKIP() << "a core profile without relaxed semantics has no client-memory arrays"; + } + std::string log; + const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint vao = MakeVertexArray(); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, kLightBytes); + glEnableVertexAttribArray(0); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(program, vao, 4, [] { glDrawArrays(GL_POINTS, 0, 1); }), kLightWords); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // 32-bit data of the other signedness is the same bits, and 16-bit data of the input's own + // signedness is untouched; both still reach the driver with the input's signedness. + TEST_F(NarrowIntegerAttributeScenario, WideDataAndMatchingDataKeepTheirBits) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kWideAndMatchedVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint wide[2] = {0xFFFFFFF0u, 7u}; + const GLushort matched[2] = {65000, 300}; + const GLuint vao = MakeVertexArray(); + MakeBuffer(wide, sizeof(wide)); + glVertexAttribIPointer(0, 2, GL_UNSIGNED_INT, 0, nullptr); + MakeBuffer(matched, sizeof(matched)); + glVertexAttribIPointer(1, 2, GL_UNSIGNED_SHORT, 0, nullptr); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + const std::vector expected = {0xFFFFFFF0u, 7u, 65000u, 300u}; + EXPECT_EQ(Capture(program, vao, 4), expected); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // Two points through one glMultiDrawArrays, read back rather than captured. + TEST_F(NarrowIntegerAttributeScenario, AMultiDrawIsCorrected) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildProgram(kRenderVertexSource, kRenderFragmentSource, {}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLubyte points[8] = {240, 200, 128, 7, 250, 129, 1, 255}; + const GLuint vao = MakeVertexArray(); + MakeBuffer(points, sizeof(points)); + glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, nullptr); + glEnableVertexAttribArray(0); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + const std::vector expected = {240u, 200u, 128u, 7u, 250u, 129u, 1u, 255u}; + EXPECT_EQ(Render(program, vao, 2, + [] { + const GLint firsts[2] = {0, 1}; + const GLsizei counts[2] = {1, 1}; + glMultiDrawArrays(GL_POINTS, firsts, counts, 2); + }), + expected); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // The integer input belongs to a separable vertex program bound through a pipeline. + TEST_F(NarrowIntegerAttributeScenario, AProgramPipelineVertexStageIsCorrected) { + if (!Ready()) GTEST_SKIP(); + const GLuint vertexStage = glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &kSeparableVertexSource); + const GLuint fragmentStage = glCreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &kSeparableFragmentSource); + ASSERT_NE(vertexStage, 0u); + ASSERT_NE(fragmentStage, 0u); + m_programs.push_back(vertexStage); + m_programs.push_back(fragmentStage); + GLint linked = GL_FALSE; + glGetProgramiv(vertexStage, GL_LINK_STATUS, &linked); + ASSERT_EQ(linked, GL_TRUE); + glGetProgramiv(fragmentStage, GL_LINK_STATUS, &linked); + ASSERT_EQ(linked, GL_TRUE); + GLuint pipeline = 0; + glGenProgramPipelines(1, &pipeline); + glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexStage); + glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentStage); + const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "pipeline setup"; + + glUseProgram(0); + glBindProgramPipeline(pipeline); + EXPECT_EQ(Render(0, unsignedArray), kLightWords); + glBindProgramPipeline(0); + glDeleteProgramPipelines(1, &pipeline); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); + } + + // Whatever the driver is handed, the application still reads back the type it declared. + TEST_F(NarrowIntegerAttributeScenario, TheArrayTypeQueryStillReportsTheDeclaredType) { + if (!Ready()) GTEST_SKIP(); + std::string log; + const GLuint program = BuildCaptureProgram(kSingleInputVertexSource, {"captured"}, &log); + ASSERT_NE(program, 0u) << log; + m_programs.push_back(program); + const GLuint unsignedArray = MakeByteArray(kLightBytes, GL_UNSIGNED_BYTE); + ASSERT_EQ(glGetError(), GL_NO_ERROR) << "vertex array setup"; + + EXPECT_EQ(Capture(program, unsignedArray, 4), kLightWords); + glBindVertexArray(unsignedArray); + GLint type = 0; + GLint integer = 0; + glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_TYPE, &type); + glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_INTEGER, &integer); + EXPECT_EQ(type, GL_UNSIGNED_BYTE); + EXPECT_EQ(integer, GL_TRUE); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + ExpectDriverArraysFitTheirInputs(); } } // namespace } // namespace MGITest