mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Feat] (GL): apply glClipControl on both backends and advertise ARB_clip_control
This commit is contained in:
@@ -320,6 +320,9 @@ namespace MobileGL {
|
||||
// GL_MAX_CULL_DISTANCES; VkPhysicalDeviceFeatures::shaderCullDistance with a non-zero
|
||||
// maxCullDistances.
|
||||
Bool CullDistance = false;
|
||||
// GL_EXT_clip_control on DirectGLES. DirectVulkan applies clip control in its own
|
||||
// position fixup and does not read this.
|
||||
Bool ClipControl = false;
|
||||
};
|
||||
|
||||
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
|
||||
|
||||
@@ -843,7 +843,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
|
||||
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray,
|
||||
AdvertisedFeatureSupport{.TessellationShader = capabilities.SupportsTessellationShader,
|
||||
.CullDistance = capabilities.MaxCullDistances > 0});
|
||||
.CullDistance = capabilities.MaxCullDistances > 0,
|
||||
.ClipControl = capabilities.SupportsClipControl});
|
||||
// Exactly what InitCapabilities forwards into the dynamic parameters: the base
|
||||
// extension and _primitive when the driver has them, _attachment not yet.
|
||||
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
|
||||
@@ -1529,6 +1530,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (features.CullDistance) {
|
||||
extensions.push_back(E_GL_ARB_cull_distance);
|
||||
}
|
||||
// glClipControl is forwarded to the driver's glClipControlEXT.
|
||||
if (features.ClipControl) {
|
||||
extensions.push_back(E_GL_ARB_clip_control);
|
||||
}
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
|
||||
@@ -4125,6 +4125,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
if (headSpanDirty) { // Clip control. GL_ARB_clip_control is only advertised where the
|
||||
// driver has GL_EXT_clip_control, so without it the state can only be the default.
|
||||
if (g_GLESCapabilities.SupportsClipControl &&
|
||||
(forceFullPush || parameters.ClipOrigin != g_syncedRenderStateParameters.ClipOrigin ||
|
||||
parameters.ClipDepthMode != g_syncedRenderStateParameters.ClipDepthMode)) {
|
||||
g_GLESFuncs.glClipControlEXT(parameters.ClipOrigin, parameters.ClipDepthMode);
|
||||
}
|
||||
}
|
||||
|
||||
if (headSpanDirty) { // Line width
|
||||
if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
|
||||
g_GLESFuncs.glLineWidth(parameters.LineWidth);
|
||||
@@ -7485,6 +7494,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// - An active transform feedback capture. GL rejects a draw issued with a program
|
||||
// other than the one that began the capture, and would otherwise append the
|
||||
// emulation's vertices to the application's buffers.
|
||||
//
|
||||
// - The application's clip control. The passes place their triangle in the default
|
||||
// lower-left, [-1, 1] clip space; an upper-left origin would mirror it vertically.
|
||||
class ScopedEmulationDrawState {
|
||||
public:
|
||||
ScopedEmulationDrawState() {
|
||||
@@ -7527,6 +7539,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrainBlitErrors();
|
||||
}
|
||||
|
||||
if (g_GLESCapabilities.SupportsClipControl) {
|
||||
g_GLESFuncs.glGetIntegerv(GL_CLIP_ORIGIN, &m_clipOrigin);
|
||||
g_GLESFuncs.glGetIntegerv(GL_CLIP_DEPTH_MODE, &m_clipDepthMode);
|
||||
g_GLESFuncs.glClipControlEXT(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
}
|
||||
|
||||
m_activeTextureUnit = TextureImpl::g_activeTextureUnit;
|
||||
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
||||
SamplerImpl::UnbindSampler(TextureImpl::TempTextureUnit);
|
||||
@@ -7597,6 +7615,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
colorMask.z() ? GL_TRUE : GL_FALSE, colorMask.w() ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
}
|
||||
if (g_GLESCapabilities.SupportsClipControl) {
|
||||
g_GLESFuncs.glClipControlEXT(static_cast<GLenum>(m_clipOrigin), static_cast<GLenum>(m_clipDepthMode));
|
||||
}
|
||||
if (m_pausedTransformFeedback && g_GLESFuncs.glResumeTransformFeedback) {
|
||||
g_GLESFuncs.glResumeTransformFeedback();
|
||||
}
|
||||
@@ -7628,6 +7649,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLint m_stencilPass[2] = {GL_KEEP, GL_KEEP};
|
||||
Uint m_activeTextureUnit = 0;
|
||||
Bool m_pausedTransformFeedback = false;
|
||||
GLint m_clipOrigin = GL_LOWER_LEFT;
|
||||
GLint m_clipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
|
||||
// The eight GL_CLIP_DISTANCE0_EXT..7_EXT entries are last so that a driver without
|
||||
// GL_EXT_clip_cull_distance can be served by shortening the count instead of asking
|
||||
// it about tokens it does not know. They belong here at all because an emulation pass
|
||||
|
||||
@@ -916,6 +916,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it
|
||||
// to OpSelect, which SPIR-V consumes directly.
|
||||
E_GL_EXT_shader_integer_mix,
|
||||
// The origin and depth mode select the Y negation and the [-1, 1] -> [0, 1] depth
|
||||
// remap of the position fixup every program already gets, so no device feature is
|
||||
// involved (ApplyClipControlToTransformFlags).
|
||||
E_GL_ARB_clip_control,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
|
||||
@@ -75,6 +75,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
|
||||
// glClipControl folded into the position fixup's two bits. An upper-left origin negates
|
||||
// clip-space Y on top of whatever the framebuffer needs (the fixup flips before it rotates,
|
||||
// so this stays in the logical orientation), and inverts the cull sense with it, which
|
||||
// keeps facing a property of the clip-space winding as the extension requires. A
|
||||
// [0, 1] depth mode is Vulkan's own clip volume, so the remap is dropped. gl_FragCoord is
|
||||
// a window coordinate, which clip control does not move.
|
||||
static constexpr Uint32 ApplyClipControlToTransformFlags(Uint32 flags, GLenum origin, GLenum depthMode) {
|
||||
if (origin == GL_UPPER_LEFT) flags ^= static_cast<Uint32>(CompileOptionBit::PositionYFlip);
|
||||
if (depthMode == GL_ZERO_TO_ONE) flags &= ~static_cast<Uint32>(CompileOptionBit::PositionZRemap);
|
||||
return flags;
|
||||
}
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
|
||||
@@ -520,8 +520,10 @@
|
||||
// not rewind an arena while this draw owns as-yet-unrecorded slices.
|
||||
WirePreparationScope preparing(m_wirePreparationDepth);
|
||||
|
||||
ProgramFactory::CompileOptionFlags flags =
|
||||
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(fbo->IsDefault));
|
||||
const RenderStateParameters& clipControl = MG_Pipe::gPipeInputs->GetRenderStateParameters();
|
||||
ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionFlags(
|
||||
ProgramFactory::ApplyClipControlToTransformFlags(GetBaseTransformFlagsRaw(fbo->IsDefault),
|
||||
clipControl.ClipOrigin, clipControl.ClipDepthMode));
|
||||
if (m_transformFeedbackFeatureEnabled && MG_Pipe::gPipeInputs->IsTransformFeedbackActive() &&
|
||||
program.GetTransformFeedbackVaryingCount() > 0)
|
||||
flags |= ProgramFactory::CompileOptionBit::XfbCapture;
|
||||
|
||||
@@ -94,6 +94,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/FencePollScenario.cpp
|
||||
Scenarios/PrimitiveRestartScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClipControlScenario.cpp
|
||||
Scenarios/ClientVertexArrayScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
# P15 S0: a clear between draws into the same framebuffer (DirectVulkan records it in the pass).
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipControlScenario.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 - glClipControl REACHES THE RASTERIZER, AND ONLY THE APPLICATION'S DRAWS.
|
||||
//
|
||||
// The state used to be recorded and reported back by glGet but never applied, so an upper-left
|
||||
// origin drew the right way up and a [0, 1] depth mode kept GL's [-1, 1] clip volume. Each case
|
||||
// below is a colour-only observable of one half of the extension:
|
||||
//
|
||||
// - origin: a primitive in the lower half of clip space lands in the TOP half of the window
|
||||
// under GL_UPPER_LEFT, on the default framebuffer (which DirectVulkan stores flipped already)
|
||||
// and on an FBO (which it does not);
|
||||
// - facing: culling follows the clip-space winding whatever the origin, as the extension
|
||||
// requires;
|
||||
// - depth: under GL_ZERO_TO_ONE a primitive at z = -0.5 is outside the clip volume, and one at
|
||||
// z = 0.25 is written as 0.25 rather than 0.625;
|
||||
// - a framebuffer blit issued while the application's origin is upper-left is a copy, not a
|
||||
// draw, and must not come out mirrored.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#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 const char* kVS = R"(#version 330 core
|
||||
in vec3 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// Red where the rasterizer calls the fragment front-facing, green where it calls it back-facing.
|
||||
constexpr const char* kFacingFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = gl_FrontFacing ? vec4(1.0, 0.0, 0.0, 1.0) : vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// Left half of clip space counter-clockwise, right half clockwise.
|
||||
constexpr float kWoundHalves[] = {
|
||||
-1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, //
|
||||
0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, //
|
||||
0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, //
|
||||
1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,
|
||||
};
|
||||
|
||||
bool ExtensionAdvertised(const char* name) {
|
||||
GLint count = 0;
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||
for (GLint i = 0; i < count; ++i) {
|
||||
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, i));
|
||||
if (extension && std::strcmp(extension, name) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Draws `vertexCount` xyz vertices as GL_TRIANGLES in red.
|
||||
void DrawTriangles(unsigned int program, const float* xyz, int vertexCount) {
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertexCount * 3 * sizeof(float)), xyz,
|
||||
GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
// The lower half of clip space (y in [-1, 0]) as two triangles at depth z.
|
||||
void DrawLowerHalf(unsigned int program, float z = 0.0f) {
|
||||
const float quad[] = {-1.0f, -1.0f, z, 1.0f, -1.0f, z, 1.0f, 0.0f, z,
|
||||
-1.0f, -1.0f, z, 1.0f, 0.0f, z, -1.0f, 0.0f, z};
|
||||
DrawTriangles(program, quad, 6);
|
||||
}
|
||||
|
||||
// The whole of clip space at depth z.
|
||||
void DrawEverything(unsigned int program, float z) {
|
||||
const float quad[] = {-1.0f, -1.0f, z, 1.0f, -1.0f, z, 1.0f, 1.0f, z,
|
||||
-1.0f, -1.0f, z, 1.0f, 1.0f, z, -1.0f, 1.0f, z};
|
||||
DrawTriangles(program, quad, 6);
|
||||
}
|
||||
|
||||
// Bottom and top halves of a `width` x `height` readback, inset away from the seam.
|
||||
::testing::AssertionResult HalvesAre(const Image& image, const char* bottom, const char* top,
|
||||
const std::string& when) {
|
||||
const int w = image.Width(), h = image.Height();
|
||||
auto lower = RegionIsMostly(image, w / 8, w - 1 - w / 8, h / 8, h / 2 - 1 - h / 8, bottom, 0.0,
|
||||
when + " (bottom half)");
|
||||
if (!lower) return lower;
|
||||
return RegionIsMostly(image, w / 8, w - 1 - w / 8, h / 2 + h / 8, h - 1 - h / 8, top, 0.0,
|
||||
when + " (top half)");
|
||||
}
|
||||
|
||||
class ClipControlScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!ExtensionAdvertised("GL_ARB_clip_control")) {
|
||||
GTEST_SKIP() << "GL_ARB_clip_control is not advertised by this backend/driver";
|
||||
}
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (m_program == 0) return;
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDeleteProgram(m_program);
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(ClipControlScenario, UpperLeftOriginMirrorsTheImageOnBothFramebufferKinds) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
ColorFbo fbo = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(fbo.fbo, 0u);
|
||||
|
||||
for (const bool toDefault : {true, false}) {
|
||||
for (const GLenum origin : {GLenum(GL_LOWER_LEFT), GLenum(GL_UPPER_LEFT)}) {
|
||||
if (toDefault) {
|
||||
BindDefaultFramebuffer();
|
||||
} else {
|
||||
BindFbo(fbo);
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClipControl(origin, GL_NEGATIVE_ONE_TO_ONE);
|
||||
GLint reported = 0;
|
||||
glGetIntegerv(GL_CLIP_ORIGIN, &reported);
|
||||
EXPECT_EQ(static_cast<GLenum>(reported), origin);
|
||||
DrawLowerHalf(m_program);
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
|
||||
const Image image = toDefault ? ReadPixels(gl.Width(), gl.Height()) : ReadPixels(64, 64);
|
||||
const std::string when = std::string(toDefault ? "default framebuffer" : "FBO") +
|
||||
(origin == GL_UPPER_LEFT ? ", GL_UPPER_LEFT" : ", GL_LOWER_LEFT");
|
||||
if (origin == GL_UPPER_LEFT) {
|
||||
EXPECT_TRUE(HalvesAre(image, "blue", "red", when));
|
||||
} else {
|
||||
EXPECT_TRUE(HalvesAre(image, "red", "blue", when));
|
||||
}
|
||||
}
|
||||
}
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(fbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(ClipControlScenario, CullingFollowsTheClipSpaceWindingUnderEitherOrigin) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ColorFbo fbo = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(fbo.fbo, 0u);
|
||||
const float* triangles = kWoundHalves;
|
||||
for (const bool toDefault : {true, false}) {
|
||||
for (const GLenum origin : {GLenum(GL_LOWER_LEFT), GLenum(GL_UPPER_LEFT)}) {
|
||||
if (toDefault) {
|
||||
BindDefaultFramebuffer();
|
||||
} else {
|
||||
BindFbo(fbo);
|
||||
}
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glEnable(GL_CULL_FACE); // GL_BACK, GL_CCW
|
||||
glClipControl(origin, GL_NEGATIVE_ONE_TO_ONE);
|
||||
DrawTriangles(m_program, triangles, 12);
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
const Image image = toDefault ? ReadPixels(Gl().Width(), Gl().Height()) : ReadPixels(64, 64);
|
||||
const int w = image.Width(), h = image.Height();
|
||||
const std::string when = std::string(toDefault ? "default framebuffer" : "FBO") +
|
||||
(origin == GL_UPPER_LEFT ? ", GL_UPPER_LEFT" : ", GL_LOWER_LEFT");
|
||||
EXPECT_TRUE(RegionIsMostly(image, w / 8, w / 2 - 1 - w / 8, h / 8, h - 1 - h / 8, "red", 0.0,
|
||||
when + ": counter-clockwise half is front-facing"));
|
||||
EXPECT_TRUE(RegionIsMostly(image, w / 2 + w / 8, w - 1 - w / 8, h / 8, h - 1 - h / 8, "blue", 0.0,
|
||||
when + ": clockwise half is culled"));
|
||||
}
|
||||
}
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(fbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
// gl_FrontFacing has to agree with culling, under either origin, on either framebuffer kind,
|
||||
// and has to follow glFrontFace. A backend that keeps facing right by swapping its cull bits
|
||||
// instead of its front face gets culling right and gl_FrontFacing backwards.
|
||||
TEST_F(ClipControlScenario, FrontFacingFollowsTheWindingAndGlFrontFace) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
std::string error;
|
||||
const unsigned int facing = CompileProgram(kVS, kFacingFS, &error);
|
||||
ASSERT_NE(facing, 0u) << error;
|
||||
ColorFbo fbo = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(fbo.fbo, 0u);
|
||||
for (const bool toDefault : {true, false}) {
|
||||
for (const GLenum origin : {GLenum(GL_LOWER_LEFT), GLenum(GL_UPPER_LEFT)}) {
|
||||
for (const GLenum frontFace : {GLenum(GL_CCW), GLenum(GL_CW)}) {
|
||||
if (toDefault) {
|
||||
BindDefaultFramebuffer();
|
||||
} else {
|
||||
BindFbo(fbo);
|
||||
}
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glFrontFace(frontFace);
|
||||
glClipControl(origin, GL_NEGATIVE_ONE_TO_ONE);
|
||||
DrawTriangles(facing, kWoundHalves, 12);
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
glFrontFace(GL_CCW);
|
||||
|
||||
const Image image = toDefault ? ReadPixels(Gl().Width(), Gl().Height()) : ReadPixels(64, 64);
|
||||
const int w = image.Width(), h = image.Height();
|
||||
const std::string when = std::string(toDefault ? "default framebuffer" : "FBO") +
|
||||
(origin == GL_UPPER_LEFT ? ", GL_UPPER_LEFT" : ", GL_LOWER_LEFT") +
|
||||
(frontFace == GL_CW ? ", GL_CW" : ", GL_CCW");
|
||||
const char* ccwColor = frontFace == GL_CCW ? "red" : "green";
|
||||
const char* cwColor = frontFace == GL_CCW ? "green" : "red";
|
||||
EXPECT_TRUE(RegionIsMostly(image, w / 8, w / 2 - 1 - w / 8, h / 8, h - 1 - h / 8, ccwColor, 0.0,
|
||||
when + ": counter-clockwise half"));
|
||||
EXPECT_TRUE(RegionIsMostly(image, w / 2 + w / 8, w - 1 - w / 8, h / 8, h - 1 - h / 8, cwColor,
|
||||
0.0, when + ": clockwise half"));
|
||||
}
|
||||
}
|
||||
}
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(fbo);
|
||||
glDeleteProgram(facing);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(ClipControlScenario, ZeroToOneDepthModeMovesTheClipVolumeAndTheMapping) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ColorFbo fbo = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(fbo.fbo, 0u);
|
||||
GLuint depth = 0;
|
||||
glGenRenderbuffers(1, &depth);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, depth);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 64, 64);
|
||||
BindFbo(fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
for (const GLenum mode : {GLenum(GL_NEGATIVE_ONE_TO_ONE), GLenum(GL_ZERO_TO_ONE)}) {
|
||||
const bool zeroToOne = mode == GL_ZERO_TO_ONE;
|
||||
const std::string when = zeroToOne ? "GL_ZERO_TO_ONE" : "GL_NEGATIVE_ONE_TO_ONE";
|
||||
|
||||
// z = -0.5 is inside [-w, w] and outside [0, w].
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClipControl(GL_LOWER_LEFT, mode);
|
||||
GLint reported = 0;
|
||||
glGetIntegerv(GL_CLIP_DEPTH_MODE, &reported);
|
||||
EXPECT_EQ(static_cast<GLenum>(reported), mode);
|
||||
DrawEverything(m_program, -0.5f);
|
||||
EXPECT_TRUE(RegionIsMostly(ReadPixels(64, 64), 8, 55, 8, 55, zeroToOne ? "blue" : "red", 0.0,
|
||||
when + ": a primitive at z = -0.5"));
|
||||
|
||||
// Against a depth buffer cleared to 0.5: z = 0.25 is written as 0.25 under [0, 1] and as
|
||||
// 0.625 under [-1, 1].
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.5);
|
||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
DrawEverything(m_program, 0.25f);
|
||||
EXPECT_TRUE(RegionIsMostly(ReadPixels(64, 64), 8, 55, 8, 55, zeroToOne ? "red" : "blue", 0.0,
|
||||
when + ": a primitive at z = 0.25 against depth 0.5"));
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
}
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glClearDepth(1.0);
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteRenderbuffers(1, &depth);
|
||||
DestroyColorFbo(fbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(ClipControlScenario, ABlitUnderAnUpperLeftOriginIsNotMirrored) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
ColorFbo fbo = MakeColorFbo(64, 64);
|
||||
ASSERT_NE(fbo.fbo, 0u);
|
||||
BindFbo(fbo);
|
||||
ClearTo(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
DrawLowerHalf(m_program);
|
||||
|
||||
glClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo.fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, 64, 64, 0, 0, gl.Width(), gl.Height(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
EXPECT_TRUE(HalvesAre(ReadPixels(gl.Width(), gl.Height()), "red", "blue", "FBO blitted to the window"));
|
||||
DestroyColorFbo(fbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -1337,6 +1337,52 @@ TEST(TessellationCapabilities, FollowTheVersionOrTheExtension) {
|
||||
EXPECT_TRUE(es32.SupportsTessellationShader);
|
||||
}
|
||||
|
||||
// glClipControl is forwarded only to a driver that both advertises GL_EXT_clip_control and
|
||||
// resolves its entry point.
|
||||
TEST(ClipControlCapabilities, NeedTheExtensionAndTheEntryPoint) {
|
||||
auto withEntryPoint = MakeFakeGLESFunctions();
|
||||
withEntryPoint.glClipControlEXT = [](GLenum, GLenum) {};
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities noExtension;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(noExtension, withEntryPoint));
|
||||
EXPECT_FALSE(noExtension.SupportsClipControl);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_clip_control");
|
||||
MobileGL::MG_External::GLESCapabilities noEntryPoint;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(noEntryPoint, MakeFakeGLESFunctions()));
|
||||
EXPECT_FALSE(noEntryPoint.SupportsClipControl);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_clip_control");
|
||||
MobileGL::MG_External::GLESCapabilities both;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(both, withEntryPoint));
|
||||
EXPECT_TRUE(both.SupportsClipControl);
|
||||
}
|
||||
|
||||
// DirectGLES advertises ARB_clip_control on the driver's extension; DirectVulkan always, since it
|
||||
// applies clip control in its own position fixup.
|
||||
TEST(CoreSurfaceAdvertisement, ClipControlFollowsTheBackendsRoute) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
EXPECT_FALSE(contains(MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false,
|
||||
false, false),
|
||||
MobileGL::E_GL_ARB_clip_control));
|
||||
const MobileGL::MG_Backend::AdvertisedFeatureSupport clipControl{.ClipControl = true};
|
||||
EXPECT_TRUE(contains(MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false,
|
||||
false, false, clipControl),
|
||||
MobileGL::E_GL_ARB_clip_control));
|
||||
EXPECT_TRUE(contains(MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false,
|
||||
false),
|
||||
MobileGL::E_GL_ARB_clip_control));
|
||||
}
|
||||
|
||||
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
|
||||
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
|
||||
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
|
||||
|
||||
@@ -111,3 +111,23 @@ void main() {
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
// glClipControl rides on the position fixup's two bits. An upper-left origin toggles the Y flip
|
||||
// (an FBO gains one, the default framebuffer loses its own), a [0, 1] depth mode drops the
|
||||
// depth remap, and nothing else - the rotation and gl_FragCoord bits - moves.
|
||||
TEST(ClipControlTransformFlags, OriginTogglesTheFlipAndDepthModeDropsTheRemap) {
|
||||
using Bit = MG_Backend::DirectVulkan::ProgramFactory::CompileOptionBit;
|
||||
const auto apply = MG_Backend::DirectVulkan::ProgramFactory::ApplyClipControlToTransformFlags;
|
||||
const Uint32 fbo = static_cast<Uint32>(Bit::PositionZRemap);
|
||||
const Uint32 window = static_cast<Uint32>(Bit::PositionZRemap) | static_cast<Uint32>(Bit::PositionYFlip) |
|
||||
static_cast<Uint32>(Bit::FragCoordYFlip) | static_cast<Uint32>(Bit::SurfaceRotate90);
|
||||
|
||||
EXPECT_EQ(apply(fbo, GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE), fbo);
|
||||
EXPECT_EQ(apply(window, GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE), window);
|
||||
EXPECT_EQ(apply(fbo, GL_UPPER_LEFT, GL_NEGATIVE_ONE_TO_ONE), fbo | static_cast<Uint32>(Bit::PositionYFlip));
|
||||
EXPECT_EQ(apply(window, GL_UPPER_LEFT, GL_NEGATIVE_ONE_TO_ONE),
|
||||
window & ~static_cast<Uint32>(Bit::PositionYFlip));
|
||||
EXPECT_EQ(apply(fbo, GL_LOWER_LEFT, GL_ZERO_TO_ONE), 0u);
|
||||
EXPECT_EQ(apply(window, GL_UPPER_LEFT, GL_ZERO_TO_ONE),
|
||||
static_cast<Uint32>(Bit::FragCoordYFlip) | static_cast<Uint32>(Bit::SurfaceRotate90));
|
||||
}
|
||||
|
||||
@@ -661,6 +661,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
INIT_GLES_FUNC(glMapBufferOES)
|
||||
INIT_GLES_FUNC_OPTIONAL(glPolygonModeNV)
|
||||
INIT_GLES_FUNC_OPTIONAL(glPolygonModeANGLE)
|
||||
INIT_GLES_FUNC_OPTIONAL(glClipControlEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glColorMaskiEXT)
|
||||
INIT_GLES_FUNC_OPTIONAL(glColorMaskiOES)
|
||||
// Extension-only multi-draw entry points. eglGetProcAddress may legally return a
|
||||
@@ -1124,6 +1125,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// comes out false, so a stub pointer counting as support would silently break the draws.
|
||||
Bool hasBaseInstanceExtension = false;
|
||||
Bool hasTessellationShaderExtension = false;
|
||||
Bool hasClipControlExtension = false;
|
||||
for (GLint i = 0; i < extCount; ++i) {
|
||||
const char* extension = (const char*)glesFuncs.glGetStringi(GL_EXTENSIONS, i);
|
||||
if (extension) {
|
||||
@@ -1215,6 +1217,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
|
||||
caps.SupportsClipDistance = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_EXT_clip_control") == 0) {
|
||||
hasClipControlExtension = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
|
||||
caps.SupportsViewportArray = true;
|
||||
}
|
||||
@@ -1287,6 +1292,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
||||
caps.SupportsTessellationShader = (esAtLeast32 || (esAtLeast31 && hasTessellationShaderExtension)) &&
|
||||
glesFuncs.glPatchParameteri != nullptr;
|
||||
// The string AND the pointer: eglGetProcAddress may hand back a stub for a name the
|
||||
// driver does not implement.
|
||||
caps.SupportsClipControl = hasClipControlExtension && glesFuncs.glClipControlEXT != nullptr;
|
||||
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
||||
glesFuncs.glMemoryBarrier != nullptr &&
|
||||
glesFuncs.glCreateShader != nullptr &&
|
||||
@@ -1319,6 +1327,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
|
||||
caps.SupportsBaseInstance ? "yes" : "no");
|
||||
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
|
||||
MGLOG_I(" clip control (EXT_clip_control): %s", caps.SupportsClipControl ? "yes" : "no");
|
||||
MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
|
||||
caps.SupportsViewportArray ? "yes" : "no");
|
||||
{
|
||||
|
||||
@@ -650,6 +650,8 @@ namespace MobileGL {
|
||||
// Extension aliases for glPolygonMode (GLES core has none) and indexed glColorMaski.
|
||||
GL_FUNC_TYPEDEF(void, glPolygonModeNV, GLenum face, GLenum mode)
|
||||
GL_FUNC_TYPEDEF(void, glPolygonModeANGLE, GLenum face, GLenum mode)
|
||||
// GL_EXT_clip_control (ES has no core glClipControl).
|
||||
GL_FUNC_TYPEDEF(void, glClipControlEXT, GLenum origin, GLenum depth)
|
||||
GL_FUNC_TYPEDEF(void, glColorMaskiEXT, GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a)
|
||||
GL_FUNC_TYPEDEF(void, glColorMaskiOES, GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a)
|
||||
|
||||
@@ -1066,6 +1068,7 @@ namespace MobileGL {
|
||||
GL_FUNC_DECL(glMapBufferOES)
|
||||
GL_FUNC_DECL(glPolygonModeNV)
|
||||
GL_FUNC_DECL(glPolygonModeANGLE)
|
||||
GL_FUNC_DECL(glClipControlEXT)
|
||||
GL_FUNC_DECL(glColorMaskiEXT)
|
||||
GL_FUNC_DECL(glColorMaskiOES)
|
||||
|
||||
@@ -1117,6 +1120,9 @@ namespace MobileGL {
|
||||
// Tessellation stages: ES 3.2 core, or GL_EXT_tessellation_shader on 3.1, with
|
||||
// glPatchParameteri resolved either way.
|
||||
Bool SupportsTessellationShader = false;
|
||||
// GL_EXT_clip_control advertised and glClipControlEXT resolved: the driver applies a
|
||||
// clip-space origin and depth mode, so the application's glClipControl is forwarded.
|
||||
Bool SupportsClipControl = false;
|
||||
// GL_EXT_texture_view / GL_OES_texture_view: two ES texture names sharing one
|
||||
// storage, i.e. the only way DirectGLES can answer glTextureView at all. ES never
|
||||
// made this core - not even in 3.2 - so unlike every other capability here there is
|
||||
|
||||
Reference in New Issue
Block a user