mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Merge] (DirectGLES): land the post-relink rebind repair
This commit is contained in:
@@ -6663,6 +6663,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} else {
|
||||
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
|
||||
}
|
||||
// The driver program was relinked IN PLACE, so its GL name no longer identifies
|
||||
// the executable behind it - and that name is exactly what Use()'s
|
||||
// g_lastUsedBackendProgramId early-out treats as identifying it. Without this,
|
||||
// a rebuild of the program that is already bound issues no glUseProgram at all
|
||||
// and the driver keeps running whatever the last one installed.
|
||||
//
|
||||
// GL 4.6 core 7.3 does promise that a successful re-link of a program in use
|
||||
// installs the new executable - but only "for all shader stages where the program
|
||||
// is active", and a stage the previous link did not produce is not active for
|
||||
// anything. So a relink that ADDS a stage is precisely the case the promise does
|
||||
// not cover. Verified with no MobileGL in the process (bare EGL + GLES 3.2, Mesa
|
||||
// 26.1.4 llvmpipe): vertex+fragment linked, used and drawn renders; a geometry
|
||||
// shader attached and relinked reports LINK_STATUS true with an empty info log,
|
||||
// and the next draw renders NOTHING and raises no error - while the same draw
|
||||
// after a fresh glUseProgram of the same name renders again.
|
||||
//
|
||||
// A flag rather than zeroing the guard: 0 is also the id Use() binds for a build
|
||||
// that did NOT come out usable, and a zeroed guard would make it skip that
|
||||
// glUseProgram(0) and leave the failed program's previous executable running -
|
||||
// the silent wrong-shader draw Use() exists to prevent.
|
||||
m_rebindAfterRelink = true;
|
||||
m_baseInstanceUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId,
|
||||
BASE_INSTANCE_UNIFORM_NAME);
|
||||
m_drawIdUniformLocation = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, DRAW_ID_UNIFORM_NAME);
|
||||
@@ -6812,7 +6833,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::Use() const {
|
||||
void BackendProgramObjectImpl::Use() {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -6822,9 +6843,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// test case's alpha that way once a sampler2DRect stage failed to
|
||||
// transpile). Bind nothing instead: the draw is then a visible no-op.
|
||||
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
|
||||
if (g_lastUsedBackendProgramId == programToBind) {
|
||||
// ...unless SyncToBackend relinked this program since the last bind, in which case
|
||||
// the id matching proves nothing about the executable behind it.
|
||||
if (g_lastUsedBackendProgramId == programToBind && !m_rebindAfterRelink) {
|
||||
return;
|
||||
}
|
||||
m_rebindAfterRelink = false;
|
||||
if (!m_backendProgramUsable) {
|
||||
// Every draw made with this program renders nothing and raises no GL error, so
|
||||
// without this line the only symptom is a framebuffer that kept its clear
|
||||
|
||||
@@ -1189,7 +1189,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BackendProgramObjectImpl();
|
||||
~BackendProgramObjectImpl();
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
void Use() const;
|
||||
void Use();
|
||||
void SetBaseInstance(Uint32 baseInstance) const;
|
||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||
void SetDrawID(Uint32 drawId) const;
|
||||
@@ -1332,6 +1332,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||
// next Use(). Use() dedupes on a GL program NAME, and a relink replaces the
|
||||
// executable behind that name without changing it - see the note at the
|
||||
// glLinkProgram in SyncToBackend for what the driver runs otherwise.
|
||||
Bool m_rebindAfterRelink = false;
|
||||
|
||||
Int m_globalUboBackendBlockIndex = -1;
|
||||
Int m_globalUboBackendBlockSize = 0;
|
||||
|
||||
@@ -104,6 +104,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RelinkStageSetScenario.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 - A RELINK MAY CHANGE WHICH STAGES A PROGRAM HAS, AND EVERY DRAW AFTER IT RUNS
|
||||
// THE NEW STAGE SET.
|
||||
//
|
||||
// GL 4.6 core 7.3: glLinkProgram builds an executable out of whatever is attached at that
|
||||
// moment, so the stage set is a property of a LINK and not of a program. A program that
|
||||
// linked vertex+fragment, drew, then had a geometry shader attached and was relinked runs
|
||||
// three stages from that point on.
|
||||
//
|
||||
// DirectGLES rebuilds its driver program in place - same GL name, new executable - and the
|
||||
// per-draw bind dedupes on that name, so a relink that changed the stage set installed
|
||||
// nothing and the following draws rendered NOTHING at all: no GL error, LINK_STATUS true,
|
||||
// and a framebuffer that kept its clear colour. See the note at the glLinkProgram in
|
||||
// BackendProgramObjectImpl::SyncToBackend for what the driver does with such a relink.
|
||||
//
|
||||
// PostLinkAttachScenario pins the other half of the same rule - that the executable does
|
||||
// NOT move until the relink. This one pins what happens when it does, in all three
|
||||
// directions: a stage added, a stage removed, and a stage added that the ES backend has to
|
||||
// synthesize a partner for.
|
||||
//
|
||||
// Every case asserts on a SHAPE and not merely on "something came out". The geometry and
|
||||
// tessellation stages here halve the triangle, so a full-viewport green frame and a
|
||||
// half-size one say which executable ran - "still drew" and "drew the right stages" are
|
||||
// different claims and only the second one is worth pinning.
|
||||
//
|
||||
// Needs a real context: what is asserted is a rendered pixel out of a backend program build.
|
||||
|
||||
#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 {
|
||||
|
||||
constexpr int kFboWidth = 64;
|
||||
constexpr int kFboHeight = 64;
|
||||
|
||||
// A full-viewport triangle out of gl_VertexID alone, so no case here needs a vertex
|
||||
// buffer and one draw covers every pixel of the target.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
void main()
|
||||
{
|
||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Halves the triangle instead of passing it through: the centre pixel stays covered
|
||||
// and all four corners fall outside, so the frame alone says whether this stage ran.
|
||||
const char* const kGeometrySource = R"(#version 420 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
gl_Position = vec4(gl_in[i].gl_Position.xy * 0.5, gl_in[i].gl_Position.zw);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// No control stage on purpose: OpenGL ES rejects that shape outright, so DirectGLES
|
||||
// synthesizes a pass-through one (AttachPassthroughTessControlStage) and DirectVulkan
|
||||
// does the same. Reading only gl_in[].gl_Position keeps this inside what such a
|
||||
// pass-through may forward. At the tessellation levels it sets (all 1.0) the patch
|
||||
// comes back out as one triangle whose gl_TessCoord values are the three corners, so
|
||||
// the barycentric sum reproduces the vertex stage's triangle - halved, for the same
|
||||
// reason the geometry stage above halves it.
|
||||
const char* const kTessEvalSource = R"(#version 420 core
|
||||
layout(triangles, equal_spacing, ccw) in;
|
||||
void main()
|
||||
{
|
||||
vec4 p = gl_TessCoord.x * gl_in[0].gl_Position +
|
||||
gl_TessCoord.y * gl_in[1].gl_Position +
|
||||
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||
gl_Position = vec4(p.xy * 0.5, p.zw);
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr Rgba8 kGreen{0, 255, 0, 255};
|
||||
constexpr Rgba8 kRed{255, 0, 0, 255};
|
||||
|
||||
class RelinkStageSetScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
m_target = MakeColorFbo(kFboWidth, kFboHeight);
|
||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
|
||||
BindFbo(m_target);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
for (const GLuint shader : m_shaders) glDeleteShader(shader);
|
||||
m_shaders.clear();
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_target);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// The same real-backend probes the other stage-gated scenarios use: 0 on a
|
||||
// DirectGLES driver without the extension and on a DirectVulkan device without
|
||||
// the feature.
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= 4;
|
||||
}
|
||||
|
||||
static bool BackendHostsTessellation() {
|
||||
GLint maxTessGenLevel = 0;
|
||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||
DrainErrors();
|
||||
return maxTessGenLevel >= 1;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
if (length <= 0) return {};
|
||||
std::string log(static_cast<size_t>(length), '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length, nullptr, log.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length, nullptr, log.data());
|
||||
}
|
||||
log.resize(std::char_traits<char>::length(log.c_str()));
|
||||
return log;
|
||||
}
|
||||
|
||||
GLuint MakeShader(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
if (shader == 0) return 0;
|
||||
m_shaders.push_back(shader);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE) << "a scenario shader did not compile: " << InfoLog(shader, true);
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint MakeProgram() {
|
||||
const GLuint program = glCreateProgram();
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
bool Link(GLuint program) {
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
ADD_FAILURE() << "the link failed: " << InfoLog(program, false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clears to red and draws. Red is the clear colour deliberately: nothing here ever
|
||||
// paints red inside the triangle, so a frame that is red where it should be green
|
||||
// says "this draw did not execute" while a frame that is green where it should be
|
||||
// red says "it executed against the wrong executable" - two failures worth telling
|
||||
// apart. The error is sampled between the draw and the readback so a rejected draw
|
||||
// is never confused with a readback that went wrong afterwards.
|
||||
Image DrawTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
|
||||
glUseProgram(program);
|
||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrainErrors();
|
||||
glDrawArrays(mode, 0, 3);
|
||||
if (outDrawError != nullptr) *outDrawError = glGetError();
|
||||
return ReadPixels(kFboWidth, kFboHeight);
|
||||
}
|
||||
|
||||
// The vertex stage's triangle covers the whole target, corners included.
|
||||
static void ExpectFullTriangle(const Image& frame, const char* what) {
|
||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
||||
for (const int y : {0, kFboHeight - 1}) {
|
||||
for (const int x : {0, kFboWidth - 1}) {
|
||||
ExpectPixel(frame, x, y, kGreen, what, "corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...and halved by a geometry or tessellation stage it no longer reaches any of
|
||||
// them, which is what makes the shape readable as "that stage ran".
|
||||
static void ExpectHalvedTriangle(const Image& frame, const char* what) {
|
||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
||||
for (const int y : {0, kFboHeight - 1}) {
|
||||
for (const int x : {0, kFboWidth - 1}) {
|
||||
ExpectPixel(frame, x, y, kRed, what, "corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ExpectPixel(const Image& frame, int x, int y, const Rgba8& expected, const char* what,
|
||||
const char* where) {
|
||||
EXPECT_EQ(frame.At(x, y), expected)
|
||||
<< what << ": " << where << " pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
ColorFbo m_target{};
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_shaders;
|
||||
};
|
||||
|
||||
// THE REGRESSION. Vertex+fragment, linked and DRAWN - which is what puts a built driver
|
||||
// program on the backend twin - then a geometry shader attached and the program
|
||||
// relinked. The halved frame is the assertion: the three-stage executable really is
|
||||
// what the next draw ran.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToAddAGeometryStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to add";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
||||
ExpectFullTriangle(before, "the draw before the relink");
|
||||
DrainErrors();
|
||||
|
||||
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked three-stage program must draw";
|
||||
ExpectHalvedTriangle(after, "the draw after the geometry stage was linked in");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The same move in the other direction, which no repair may confuse with "the stage
|
||||
// set did not change": the geometry stage leaves the executable, so the halving has to
|
||||
// stop with it.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToRemoveAGeometryStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsGeometry()) {
|
||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to remove";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
const GLuint geometry = MakeShader(GL_GEOMETRY_SHADER, kGeometrySource);
|
||||
glAttachShader(program, geometry);
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
// Also the control for the case above: a three-stage program linked in ONE go and
|
||||
// never relinked draws its halved triangle.
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the three-stage draw must execute";
|
||||
ExpectHalvedTriangle(before, "the draw before the geometry stage was dropped");
|
||||
DrainErrors();
|
||||
|
||||
glDetachShader(program, geometry);
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked vertex+fragment program must draw";
|
||||
ExpectFullTriangle(after, "the draw after the geometry stage was dropped");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The third direction, and the one that asks the most of the rebuild: the added stage
|
||||
// is a tessellation evaluation shader with no control stage, so the ES backend has to
|
||||
// synthesize a pass-through control stage for an executable that had neither a moment
|
||||
// ago. GL_PATCHES becomes the only legal mode with it, which is also the only draw-mode
|
||||
// change any case here makes.
|
||||
TEST_F(RelinkStageSetScenario, RelinkingToAddATessEvalStageRunsTheNewExecutable) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!BackendHostsTessellation()) {
|
||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
||||
<< "); there is no stage to add";
|
||||
}
|
||||
|
||||
const GLuint program = MakeProgram();
|
||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
DrainErrors();
|
||||
|
||||
GLenum beforeError = GL_NO_ERROR;
|
||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
||||
ExpectFullTriangle(before, "the draw before the relink");
|
||||
DrainErrors();
|
||||
|
||||
glAttachShader(program, MakeShader(GL_TESS_EVALUATION_SHADER, kTessEvalSource));
|
||||
ASSERT_TRUE(Link(program));
|
||||
// Three, which is already the default; spelled out because the synthesized control
|
||||
// stage's output patch size is compiled from it.
|
||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
||||
DrainErrors();
|
||||
|
||||
GLenum afterError = GL_NO_ERROR;
|
||||
const Image after = DrawTriangle(program, GL_PATCHES, &afterError);
|
||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked tessellating program must draw";
|
||||
ExpectHalvedTriangle(after, "the draw after the tessellation stage was linked in");
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user