mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Merge] (DirectGLES, ShaderTranspiler): land GL43 wave4 with the interface-block rename inside the L2 boundary
This commit is contained in:
@@ -45,6 +45,31 @@ namespace {
|
||||
GLint maxFragmentSsboBlocks = 9;
|
||||
bool tessAndGeometrySsboBlocksQueried = false;
|
||||
bool perStageSsboBlockQueryRaisesError = false;
|
||||
// GL_MAX_CLIP_DISTANCES. Not ES core in any version - it exists only as
|
||||
// GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance - so asking a driver without
|
||||
// the extension raises GL_INVALID_ENUM and leaves the out-param untouched. The "queried"
|
||||
// flag is what pins the gating; the "raises error" knob is what pins the drain.
|
||||
GLint maxClipDistances = 8;
|
||||
bool maxClipDistancesQueried = false;
|
||||
bool clipDistanceQueryRaisesError = false;
|
||||
// GL_MAX_VIEWPORTS / GL_VIEWPORT_SUBPIXEL_BITS / GL_VIEWPORT_BOUNDS_RANGE are
|
||||
// GL_OES_viewport_array state and, like the clip-distance pname, exist nowhere in ES core.
|
||||
GLint maxViewports = 32;
|
||||
GLint viewportSubpixelBits = 8;
|
||||
bool viewportArrayLimitsQueried = false;
|
||||
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core; GL_VIEWPORT_INDEX_PROVOKING_VERTEX comes with
|
||||
// GL_OES_viewport_array. Both must go unasked where they do not exist, and a driver answer
|
||||
// outside the four legal conventions must not be forwarded as one.
|
||||
GLint layerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
|
||||
GLint viewportIndexProvokingVertex = GL_LAST_VERTEX_CONVENTION;
|
||||
bool layerProvokingVertexQueried = false;
|
||||
// A driver rejecting one of the UNCONDITIONAL probes. GL_SMOOTH_LINE_WIDTH_RANGE is the
|
||||
// realistic one - it is desktop-only state that every GLES driver refuses - and it stands
|
||||
// in for the whole run: whatever it leaves behind must not reach the application.
|
||||
bool smoothLineWidthQueryRaisesError = false;
|
||||
// What the driver answers for the four multisample ceilings. Zero is the value that has
|
||||
// to be floored away: the frontend would otherwise advertise a sample count it rejects.
|
||||
GLint multisampleCeiling = 4;
|
||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||
GLint fragmentInterpolationOffsetBits = 6;
|
||||
@@ -160,6 +185,37 @@ namespace {
|
||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||
*data = g_fake.maxComputeImageUniforms;
|
||||
break;
|
||||
case GL_MAX_CLIP_DISTANCES:
|
||||
g_fake.maxClipDistancesQueried = true;
|
||||
if (g_fake.clipDistanceQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
*data = g_fake.maxClipDistances;
|
||||
}
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
g_fake.viewportArrayLimitsQueried = true;
|
||||
*data = g_fake.maxViewports;
|
||||
break;
|
||||
case GL_VIEWPORT_SUBPIXEL_BITS:
|
||||
g_fake.viewportArrayLimitsQueried = true;
|
||||
*data = g_fake.viewportSubpixelBits;
|
||||
break;
|
||||
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
||||
g_fake.viewportArrayLimitsQueried = true;
|
||||
*data = g_fake.viewportIndexProvokingVertex;
|
||||
break;
|
||||
case GL_LAYER_PROVOKING_VERTEX:
|
||||
g_fake.layerProvokingVertexQueried = true;
|
||||
*data = g_fake.layerProvokingVertex;
|
||||
break;
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||
case GL_MAX_FRAMEBUFFER_SAMPLES:
|
||||
case GL_MAX_INTEGER_SAMPLES:
|
||||
case GL_MAX_SAMPLES:
|
||||
*data = g_fake.multisampleCeiling;
|
||||
break;
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||
@@ -239,11 +295,22 @@ namespace {
|
||||
data[0] = g_fake.maxFragmentInterpolationOffset;
|
||||
}
|
||||
break;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
if (g_fake.smoothLineWidthQueryRaisesError) {
|
||||
g_fake.pendingError = GL_INVALID_ENUM;
|
||||
} else {
|
||||
data[0] = 0.0f;
|
||||
data[1] = 0.0f;
|
||||
}
|
||||
break;
|
||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||
g_fake.viewportArrayLimitsQueried = true;
|
||||
data[0] = 0.0f;
|
||||
data[1] = 0.0f;
|
||||
break;
|
||||
// Two-component range queries.
|
||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
case GL_ALIASED_POINT_SIZE_RANGE:
|
||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||
data[0] = 0.0f;
|
||||
data[1] = 0.0f;
|
||||
break;
|
||||
@@ -642,6 +709,186 @@ TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheS
|
||||
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// GL_MAX_CLIP_DISTANCES is the same defect as the per-stage storage blocks above, one pname
|
||||
// over: the query does not exist without GL_EXT_clip_cull_distance, so an unguarded probe left
|
||||
// an optimistic 8 behind on every ARM driver. Advertising eight clip planes a driver cannot host
|
||||
// does not make gl_ClipDistance work - SPIRV-Cross emits it behind an `#extension ... : require`
|
||||
// the ESSL compiler rejects, DirectGLES has nowhere to put the per-distance enables, and the
|
||||
// draw renders nothing while LINK_STATUS says everything is fine.
|
||||
TEST(ClipDistanceCapabilities, NoExtensionMeansNoClipDistancesAndNoQuery) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_FALSE(caps.SupportsClipDistance);
|
||||
EXPECT_EQ(caps.MaxClipDistances, 0);
|
||||
EXPECT_FALSE(g_fake.maxClipDistancesQueried)
|
||||
<< "GL_MAX_CLIP_DISTANCES is not ES core; asking for it without the extension only leaks "
|
||||
"a GL_INVALID_ENUM";
|
||||
}
|
||||
|
||||
// The other half of the same claim, and the one that keeps this from being a blanket zero: a
|
||||
// driver that HAS the extension must have its real limit come through untouched. Adreno does,
|
||||
// and it passes the clip-distance conformance cases on the strength of it.
|
||||
TEST(ClipDistanceCapabilities, TheExtensionIsQueriedAndItsLimitIsReportedVerbatim) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
|
||||
g_fake.maxClipDistances = 6;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_TRUE(caps.SupportsClipDistance);
|
||||
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
|
||||
EXPECT_EQ(caps.MaxClipDistances, 6);
|
||||
}
|
||||
|
||||
// A driver that advertises the extension and then refuses the query is a driver fault, not a
|
||||
// missing feature - but the answer has to be the honest zero either way, and the error must not
|
||||
// be left for the application's first glGetError to find.
|
||||
TEST(ClipDistanceCapabilities, ARejectedQueryIsDrainedAndReportsZero) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
|
||||
g_fake.clipDistanceQueryRaisesError = true;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
|
||||
EXPECT_EQ(caps.MaxClipDistances, 0);
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR) << "the failed query must not leave an error behind";
|
||||
}
|
||||
|
||||
// The same defect one more time, for the three GL_OES_viewport_array pnames. Their advertised
|
||||
// values do not come from the driver (GL_Getter answers GL_MAX_VIEWPORTS from the frontend state
|
||||
// width and floors GL_SUBPIXEL_BITS at its own constant), so what this pins is the other half of
|
||||
// the class defect: a pname that does not exist must not be asked for, because the GL_INVALID_ENUM
|
||||
// it raises is then attributed to whatever the application calls next.
|
||||
TEST(ViewportArrayCapabilities, TheLimitsAreOnlyAskedForWhenTheExtensionIsPresent) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities withoutCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withoutCaps, funcs));
|
||||
EXPECT_FALSE(withoutCaps.SupportsViewportArray);
|
||||
EXPECT_FALSE(g_fake.viewportArrayLimitsQueried);
|
||||
EXPECT_EQ(withoutCaps.MaxViewports, 16) << "the OpenGL core minimum, not a driver answer";
|
||||
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMin, -32768.0f);
|
||||
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMax, 32767.0f);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_OES_viewport_array");
|
||||
MobileGL::MG_External::GLESCapabilities withCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withCaps, funcs));
|
||||
EXPECT_TRUE(withCaps.SupportsViewportArray);
|
||||
EXPECT_TRUE(g_fake.viewportArrayLimitsQueried);
|
||||
EXPECT_EQ(withCaps.MaxViewports, g_fake.maxViewports);
|
||||
EXPECT_EQ(withCaps.ViewportSubpixelBits, g_fake.viewportSubpixelBits);
|
||||
}
|
||||
|
||||
// GL_LAYER_PROVOKING_VERTEX and GL_VIEWPORT_INDEX_PROVOKING_VERTEX name which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. MobileGL used to answer a hard-coded
|
||||
// GL_LAST_VERTEX_CONVENTION for both, derived from nothing, and got it wrong on both test devices
|
||||
// in OPPOSITE directions. GL_UNDEFINED_VERTEX is a legal answer (GL 4.6 table 23.65) and it is
|
||||
// the honest one wherever the capability that would give the convention meaning is absent.
|
||||
TEST(ProvokingVertexConventions, AreTakenFromTheDriverOnlyWhereThePnameExists) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
// ES 3.1, no viewport array: neither pname exists, so neither is asked for.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es31Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
||||
EXPECT_FALSE(g_fake.layerProvokingVertexQueried);
|
||||
EXPECT_EQ(es31Caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
EXPECT_EQ(es31Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
|
||||
// ES 3.2 with the viewport array: both exist and both driver answers come through verbatim.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.extensions.emplace_back("GL_OES_viewport_array");
|
||||
MobileGL::MG_External::GLESCapabilities es32Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
||||
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
|
||||
EXPECT_EQ(es32Caps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||
EXPECT_EQ(es32Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_LAST_VERTEX_CONVENTION));
|
||||
|
||||
// ES 3.2 WITHOUT the viewport array - the shape of both test devices. The layer convention is
|
||||
// real and comes from the driver; the viewport-index one describes a selection that never
|
||||
// happens, because only viewport 0 is ever rasterized, and stays undefined.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 2;
|
||||
MobileGL::MG_External::GLESCapabilities deviceLikeCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(deviceLikeCaps, funcs));
|
||||
EXPECT_EQ(deviceLikeCaps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||
EXPECT_EQ(deviceLikeCaps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
}
|
||||
|
||||
// A driver answering something that is not one of the four legal conventions must not have it
|
||||
// forwarded as one: GL_UNDEFINED_VERTEX describes "MobileGL cannot tell you" exactly.
|
||||
TEST(ProvokingVertexConventions, AnIllegalDriverAnswerBecomesUndefined) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 2;
|
||||
g_fake.layerProvokingVertex = 0x1234;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
|
||||
EXPECT_EQ(caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
}
|
||||
|
||||
// The multisample ceilings are ES 3.1 state; a driver that answers zero - or an older context
|
||||
// that answers nothing - must not have that reach GL_Getter, which would then reject the sample
|
||||
// count it just advertised.
|
||||
TEST(MultisampleCapabilities, TheAdvertisedSampleCountsNeverFallBelowOne) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.multisampleCeiling = 0;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_EQ(caps.MaxColorTextureSamples, 1);
|
||||
EXPECT_EQ(caps.MaxDepthTextureSamples, 1);
|
||||
EXPECT_EQ(caps.MaxFramebufferSamples, 1);
|
||||
EXPECT_EQ(caps.MaxIntegerSamples, 1);
|
||||
EXPECT_EQ(caps.MaxSamples, 1);
|
||||
EXPECT_EQ(caps.MaxSampleMaskWords, 1);
|
||||
}
|
||||
|
||||
// The whole point of the drain, stated once at the level that matters: capability init is the
|
||||
// first thing that ever touches the driver, so an error it leaves behind surfaces at the
|
||||
// APPLICATION's first glGetError and is blamed on an unrelated call. GL_SMOOTH_LINE_WIDTH_RANGE
|
||||
// is the stand-in because it is desktop-only state that every real GLES driver refuses.
|
||||
TEST(CapabilityProbeHygiene, ARejectedUnconditionalProbeLeavesNoErrorBehind) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.smoothLineWidthQueryRaisesError = true;
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||
|
||||
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR)
|
||||
<< "capability init must not hand the application an error it never caused";
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||
const auto funcs = MakeFakeGLESFunctions();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include "Includes.h"
|
||||
@@ -267,6 +268,116 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
|
||||
ASSERT_EQ(actual, expected);
|
||||
}
|
||||
|
||||
// GL_MIN_MAP_BUFFER_ALIGNMENT is a promise about POINTERS, and MobileGL used to keep only the
|
||||
// query half of it: glGetIntegerv answered 64 while every mapped pointer came out of a plain
|
||||
// std::vector, aligned to alignof(std::max_align_t) - 16 on aarch64. GL 4.2 /
|
||||
// ARB_map_buffer_alignment fix the minimum at 64, so under-reporting is not available and the
|
||||
// implementation has to be brought up to the number instead. Note the two different constraints:
|
||||
// glMapBuffer's pointer must be aligned outright, while glMapBufferRange's must be aligned AFTER
|
||||
// subtracting the offset the caller asked for - i.e. it sits at the offset's own alignment phase.
|
||||
// KHR-GLxx.map_buffer_alignment.functional asserts exactly these two, at offset 63, for 24
|
||||
// storage-flag combinations across 14 targets, and failed identically on both test devices.
|
||||
TEST_F(BufferTest, MappedPointersHonourTheAdvertisedMapBufferAlignment) {
|
||||
GLint advertisedAlignment = 0;
|
||||
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MIN_MAP_BUFFER_ALIGNMENT, &advertisedAlignment);
|
||||
ASSERT_EQ(advertisedAlignment, static_cast<GLint>(MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT))
|
||||
<< "the query and the allocator must read the same constant";
|
||||
ASSERT_GE(advertisedAlignment, 64) << "GL 4.2 fixes the minimum at 64";
|
||||
const SizeT alignment = static_cast<SizeT>(advertisedAlignment);
|
||||
|
||||
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||
Vector<Uint> bufferNames;
|
||||
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
|
||||
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
|
||||
slot.Bind(bufObj);
|
||||
|
||||
// The conformance test's own shape: a buffer two alignments long, mapped from the last byte
|
||||
// inside the first alignment - the offset most likely to expose a base-aligned-only fix.
|
||||
const SizeT bufferSize = 2 * alignment;
|
||||
const SizeT offset = alignment - 1;
|
||||
bufObj->Resize(bufferSize);
|
||||
Vector<Uint8> initData(bufferSize);
|
||||
for (SizeT i = 0; i < bufferSize; ++i) initData[i] = static_cast<Uint8>(i);
|
||||
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
|
||||
|
||||
const auto addressOf = [](const void* pointer) { return reinterpret_cast<std::uintptr_t>(pointer); };
|
||||
|
||||
// glMapBuffer, read-only: the shadow base itself is handed out.
|
||||
void* readMapped = bufObj->AcquireMemory(true, true, false);
|
||||
ASSERT_NE(readMapped, nullptr);
|
||||
EXPECT_EQ(addressOf(readMapped) % alignment, 0u) << "glMapBuffer(GL_READ_ONLY) returned an unaligned pointer";
|
||||
bufObj->ReleaseMemory();
|
||||
|
||||
// glMapBuffer, write: the staging store is handed out instead.
|
||||
void* writeMapped = bufObj->AcquireMemory(true, false, true);
|
||||
ASSERT_NE(writeMapped, nullptr);
|
||||
EXPECT_EQ(addressOf(writeMapped) % alignment, 0u) << "glMapBuffer(GL_WRITE_ONLY) returned an unaligned pointer";
|
||||
EXPECT_EQ(bufObj->GetMappedPointer(), writeMapped)
|
||||
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
|
||||
bufObj->ReleaseMemory();
|
||||
|
||||
// glMapBufferRange, read-only: shadow base + offset, so the phase falls out for free.
|
||||
const Range1D mapRange{.start = offset, .end = bufferSize};
|
||||
void* rangeRead = bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Read);
|
||||
ASSERT_NE(rangeRead, nullptr);
|
||||
EXPECT_EQ((addressOf(rangeRead) - offset) % alignment, 0u)
|
||||
<< "glMapBufferRange(READ) returned a pointer whose base is unaligned";
|
||||
bufObj->ReleaseMemory();
|
||||
|
||||
// glMapBufferRange, write: the staging store has to be biased to the same phase, and the
|
||||
// write-back has to follow the bias or the bytes land at the wrong place in the shadow.
|
||||
Uint8* rangeWrite = static_cast<Uint8*>(bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Write));
|
||||
ASSERT_NE(rangeWrite, nullptr);
|
||||
EXPECT_EQ((addressOf(rangeWrite) - offset) % alignment, 0u)
|
||||
<< "glMapBufferRange(WRITE) returned a pointer whose base is unaligned";
|
||||
EXPECT_EQ(bufObj->GetMappedPointer(), rangeWrite)
|
||||
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
|
||||
// Seeded from the shadow, so the mapped view starts at the offset's byte.
|
||||
EXPECT_EQ(rangeWrite[0], static_cast<Uint8>(offset));
|
||||
rangeWrite[0] = 0xAB;
|
||||
rangeWrite[bufferSize - offset - 1] = 0xCD;
|
||||
bufObj->ReleaseMemory();
|
||||
|
||||
Vector<Uint8> readBack(bufferSize);
|
||||
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
|
||||
EXPECT_EQ(readBack[offset], 0xAB) << "the biased staging write-back landed at the wrong offset";
|
||||
EXPECT_EQ(readBack[bufferSize - 1], 0xCD) << "the biased staging write-back landed at the wrong offset";
|
||||
EXPECT_EQ(readBack[offset - 1], static_cast<Uint8>(offset - 1)) << "the write-back overran the mapped range";
|
||||
}
|
||||
|
||||
// The explicit-flush path reads through the same bias, one flush offset further in: a flush of
|
||||
// [offset + 4, offset + 8) must copy the bytes the application wrote at rangeWrite[4..8), not the
|
||||
// ones sitting four bytes into the raw allocation.
|
||||
TEST_F(BufferTest, ExplicitFlushOfARangeMapFollowsTheAlignmentBias) {
|
||||
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||
Vector<Uint> bufferNames;
|
||||
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
|
||||
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
|
||||
slot.Bind(bufObj);
|
||||
|
||||
const SizeT alignment = MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT;
|
||||
const SizeT bufferSize = 2 * alignment;
|
||||
const SizeT offset = alignment - 1;
|
||||
bufObj->Resize(bufferSize);
|
||||
Vector<Uint8> initData(bufferSize, 0);
|
||||
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
|
||||
|
||||
const Range1D mapRange{.start = offset, .end = bufferSize};
|
||||
Uint8* mapped = static_cast<Uint8*>(bufObj->AcquireMemoryRange(
|
||||
mapRange, BufferMappingAccessBit::Write | BufferMappingAccessBit::FlushExplicit));
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
mapped[4] = 0x5A;
|
||||
mapped[5] = 0x5B;
|
||||
bufObj->FlushMemoryRange(4, 2);
|
||||
bufObj->ReleaseMemory();
|
||||
|
||||
Vector<Uint8> readBack(bufferSize);
|
||||
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
|
||||
EXPECT_EQ(readBack[offset + 4], 0x5A);
|
||||
EXPECT_EQ(readBack[offset + 5], 0x5B);
|
||||
EXPECT_EQ(readBack[offset + 3], 0x00) << "the explicit flush copied bytes outside the flushed range";
|
||||
}
|
||||
|
||||
TEST_F(BufferTest, CopyBufferSubData) {
|
||||
auto& srcSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyRead);
|
||||
auto& dstSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyWrite);
|
||||
|
||||
@@ -643,6 +643,55 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
||||
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
||||
}
|
||||
|
||||
// maxClipDistances is a LIMIT every Vulkan device reports; declaring ClipDistance in a module
|
||||
// needs the shaderClipDistance FEATURE, which is separate and which VulkanRenderer enables only
|
||||
// where the physical device has it. Forwarding the limit without the feature advertises eight
|
||||
// clip planes no shader may use - the same shape as the image-uniform limits above, and the same
|
||||
// shape as the GL_EXT_clip_cull_distance lie on DirectGLES. Not a blanket zero: a device WITH the
|
||||
// feature keeps its real number.
|
||||
TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||
MG_External::VulkanCapabilities caps;
|
||||
caps.MaxClipDistances = 8;
|
||||
|
||||
caps.SupportsShaderClipDistance = false;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
|
||||
|
||||
caps.SupportsShaderClipDistance = true;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||
}
|
||||
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
|
||||
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
|
||||
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
|
||||
// reports GL_UNDEFINED_VERTEX, which GL 4.6 table 23.65 permits and which is what the backend
|
||||
// honestly implements - the provoking mode is chosen per pipeline out of VK_EXT_provoking_vertex,
|
||||
// provokingVertexModePerPipeline and the topology.
|
||||
TEST(ProvokingVertexConventions, EachBackendReportsWhatItActuallyPins) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
|
||||
MG_External::GLESCapabilities glesCaps;
|
||||
glesCaps.LayerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
|
||||
glesCaps.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
|
||||
EXPECT_EQ(glesBackend.GetDynamicParameters().LayerProvokingVertex,
|
||||
static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||
EXPECT_EQ(glesBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
|
||||
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
|
||||
MG_External::VulkanCapabilities vkCaps;
|
||||
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
EXPECT_EQ(vkBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
|
||||
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||
}
|
||||
|
||||
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
|
||||
using namespace MobileGL;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ add_executable(
|
||||
EmulateSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
)
|
||||
|
||||
@@ -627,6 +627,9 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
|
||||
// real drivers produce (z = 64 on ES against 1024 elsewhere).
|
||||
{"params.MaxComputeTextureImageUnits",
|
||||
[](CompileEnv& e) { e.params.MaxComputeTextureImageUnits += 1; }},
|
||||
// wave4's 4fc3531d: glslang rejects gl_ClipDistance[i] past this at parse AND expands
|
||||
// gl_MaxClipDistances from it, so it is both a compile gate and a baked constant.
|
||||
{"params.MaxClipDistances", [](CompileEnv& e) { e.params.MaxClipDistances += 1; }},
|
||||
{"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }},
|
||||
{"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }},
|
||||
{"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }},
|
||||
@@ -830,6 +833,8 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
const std::set<String> xfbBlocks{"StageData"};
|
||||
const UnorderedMap<String, Uint> imageFormats{{"gImage", 0x8236u /*GL_R32UI*/}};
|
||||
const UnorderedMap<String, Int> storageBindings{{"Data", 3}};
|
||||
const std::map<String, String> ioBlockRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
|
||||
const std::map<String, String> otherIoBlockRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio2"}};
|
||||
|
||||
const EsslTranslationKeyInputs base = BaselineEsslInputs(spirv);
|
||||
const TranslationCacheKey baseKey = BuildEsslTranslationKey(base);
|
||||
@@ -898,6 +903,31 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
v.esslVersion = 300;
|
||||
variants.emplace_back("esslVersion", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // SpvcSession::SetAtomicCounterBlockBindings - printed into the layout(binding=)
|
||||
// qualifier of every synthesized counter block, so it changes the emitted text.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.atomicCounterEsslBindingTop = 6;
|
||||
variants.emplace_back("atomicCounterEsslBindingTop", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // the two arguments to UniquifyIoBlockNamesForEssl. Separate cases, because a stage
|
||||
// that CONSUMES a block renames it after the previous stage while one that PRODUCES
|
||||
// it renames after itself - so the same block name legitimately maps to different
|
||||
// spellings in the two maps, and a key that folded them together would let a
|
||||
// consumer's plan be served to a producer.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.inputBlockRenames = &ioBlockRenames;
|
||||
variants.emplace_back("inputBlockRenames", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.outputBlockRenames = &ioBlockRenames;
|
||||
variants.emplace_back("outputBlockRenames", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // ...and a DIFFERENT target spelling for the same block name must not share either.
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.outputBlockRenames = &otherIoBlockRenames;
|
||||
variants.emplace_back("outputBlockRenames(other target)", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.enableSpirvValidation = true;
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/UniquifyIoBlockNamesTest.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
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
tools.Disassemble(spirv, &text);
|
||||
return text;
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
// The tessellation evaluation stage of
|
||||
// KHR-GL42/43.shading_language_420pack.length_of_vector_and_matrix_* and
|
||||
// .qualifier_order_block_*, reduced to the shape that matters: ONE block name used for
|
||||
// both the block this stage consumes and the block it produces. Legal desktop GLSL - the
|
||||
// input and output block namespaces are separate - and something SPIRV-Cross re-emits
|
||||
// verbatim, so the ESSL it produces declares two different blocks called TCSOutputBlock.
|
||||
const char* kCollidingTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
in vec4 tcs_tes_result[];
|
||||
out vec4 tes_gs_result;
|
||||
|
||||
in TCSOutputBlock {
|
||||
vec4 tcs_tes_variable;
|
||||
} input_block[];
|
||||
out TCSOutputBlock {
|
||||
vec4 tes_gs_variable;
|
||||
} output_block;
|
||||
|
||||
void main()
|
||||
{
|
||||
tes_gs_result = tcs_tes_result[0];
|
||||
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||
}
|
||||
)";
|
||||
|
||||
// The same stage with the two blocks already named apart, which is the overwhelmingly
|
||||
// common shape and the one that must go through untouched.
|
||||
const char* kDistinctTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
in vec4 tcs_tes_result[];
|
||||
out vec4 tes_gs_result;
|
||||
|
||||
in TCSOutputBlock {
|
||||
vec4 tcs_tes_variable;
|
||||
} input_block[];
|
||||
out TESOutputBlock {
|
||||
vec4 tes_gs_variable;
|
||||
} output_block;
|
||||
|
||||
void main()
|
||||
{
|
||||
tes_gs_result = tcs_tes_result[0];
|
||||
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||
}
|
||||
)";
|
||||
|
||||
// gl_PerVertex is an Input block AND an Output block of one name in every tessellation
|
||||
// and geometry stage. It is the language's block, not the shader's, so it must never be
|
||||
// reported and never be renamed.
|
||||
const char* kBuiltinBlockOnlyTessEvalSource = R"(#version 420 core
|
||||
layout(isolines, point_mode) in;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class UniquifyIoBlockNamesTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the renamed module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, ProbeReportsABlockNameUsedInBothDirections) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> colliding;
|
||||
std::set<String> declared;
|
||||
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||
|
||||
EXPECT_EQ(colliding, (std::set<String>{"TCSOutputBlock"}));
|
||||
// The name set the caller picks a replacement out of has to contain what the module
|
||||
// already spells, or the replacement could land on top of an existing declaration.
|
||||
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
|
||||
EXPECT_NE(declared.find("input_block"), declared.end());
|
||||
EXPECT_NE(declared.find("output_block"), declared.end());
|
||||
}
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, ProbeIgnoresAStageWhoseBlocksAlreadyHaveDistinctNames) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> colliding;
|
||||
std::set<String> declared;
|
||||
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||
|
||||
EXPECT_TRUE(colliding.empty());
|
||||
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
|
||||
}
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, ProbeNeverReportsTheBuiltinBlock) {
|
||||
const Vector<Uint32> input =
|
||||
CompileToSpirv(GL_TESS_EVALUATION_SHADER, kBuiltinBlockOnlyTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
std::set<String> colliding;
|
||||
std::set<String> declared;
|
||||
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||
|
||||
// gl_PerVertex is read through gl_in and written through gl_Position, i.e. it is exactly
|
||||
// the in-and-out-under-one-name shape - and renaming it would invent a block no driver
|
||||
// knows.
|
||||
EXPECT_TRUE(colliding.empty()) << "gl_PerVertex must never enter the rename plan";
|
||||
}
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, RenamesTheTwoBlocksApartInTheEmittedEssl) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
// The generated ESSL really does declare the block twice under one name before the fix -
|
||||
// pinning the defect, not just the repair.
|
||||
const String before = Transpile(input);
|
||||
EXPECT_NE(before.find("in TCSOutputBlock"), String::npos) << before;
|
||||
EXPECT_NE(before.find("out TCSOutputBlock"), String::npos) << before;
|
||||
|
||||
// The plan the DirectGLES program build makes for a five-stage program: what this stage
|
||||
// consumes is spelled after the tessellation control stage (pipeline index 1) and what it
|
||||
// produces after itself (pipeline index 2).
|
||||
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
|
||||
const std::map<String, String> outputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio2"}};
|
||||
|
||||
std::set<String> renamed;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, outputRenames, renamed,
|
||||
output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_EQ(dis.find("\"TCSOutputBlock\""), String::npos)
|
||||
<< "the colliding name is still on a block struct:\n"
|
||||
<< dis;
|
||||
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_NE(after.find("TCSOutputBlock_mgio1"), String::npos) << after;
|
||||
EXPECT_NE(after.find("TCSOutputBlock_mgio2"), String::npos) << after;
|
||||
// Only the block TYPE name moves: the instance names are what the body reads and writes
|
||||
// through, and the member names are half of what ES matches the interface by.
|
||||
EXPECT_NE(after.find("input_block"), String::npos) << after;
|
||||
EXPECT_NE(after.find("output_block"), String::npos) << after;
|
||||
EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after;
|
||||
EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after;
|
||||
}
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, RenamesOnlyTheDirectionTheCallerPlanned) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// A separate-shader-objects program that ends at this stage plans no output rename,
|
||||
// because the block's consumer lives in another program that never saw the plan.
|
||||
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
|
||||
|
||||
std::set<String> renamed;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(
|
||||
ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, {}, renamed, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
|
||||
// The output block keeps the name the other program still spells.
|
||||
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
|
||||
EXPECT_EQ(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
|
||||
}
|
||||
|
||||
TEST_F(UniquifyIoBlockNamesTest, ReportsNothingWhenThePlanNamesNoBlockThisStageDeclares) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
const std::map<String, String> renames{{"SomeOtherBlock", "SomeOtherBlock_mgio2"}};
|
||||
|
||||
std::set<String> renamed;
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, renames, renames, renamed, output, true));
|
||||
// Empty is what tells the DirectGLES program build to keep the module it already had
|
||||
// instead of adopting the optimizer's re-serialised copy.
|
||||
EXPECT_TRUE(renamed.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("\"TESOutputBlock\""), String::npos) << dis;
|
||||
}
|
||||
@@ -3197,11 +3197,19 @@ TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
|
||||
GLenum type;
|
||||
};
|
||||
const Case cases[] = {
|
||||
// Legacy <=8-bit-per-channel formats store as UNorm8 component arrays.
|
||||
{GL_R3_G3_B2, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGB4, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGB5, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGBA2, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||
// Legacy <=8-bit-per-channel DESKTOP-ONLY formats store as UNorm8 component arrays, in the
|
||||
// 8-bit-per-channel ES format that layout already is. Storing them in the narrower
|
||||
// GL_RGB565/GL_RGBA4 they nominally fit in made the driver requantize the shadow bytes on
|
||||
// every upload, which is not lossless: 5-bit 2 -> UNorm8 16 -> 16/255*31 = 1.945, which a
|
||||
// truncating driver reads back as 1 (KHR-GL43.copy_image rgb4->rgb4, 12/12 failing on Mali).
|
||||
{GL_R3_G3_B2, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGB4, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGB5, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||
{GL_RGBA2, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||
// The two that are ES formats in their own right keep their native storage: an application
|
||||
// that asks for GL_RGBA4 or GL_RGB5_A1 is asking for the smaller image, and the same
|
||||
// normalization also picks the storage for glRenderbufferStorage, where those two are
|
||||
// ordinary ES render targets rather than a desktop-compatibility shim.
|
||||
{GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||
// 10/12-bit channels store as UNorm16 component arrays.
|
||||
@@ -3860,6 +3868,131 @@ TEST_F(TextureTest, ColorAttachableTargetsRequestTheThreeChannelWidening) {
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
|
||||
// ...and neither can an 8-bit one. That half of the answer used to be missing entirely, which
|
||||
// is why an R8_SNORM / RG8_SNORM colour attachment got no substitute at all on a driver
|
||||
// without EXT_render_snorm.
|
||||
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noSnormCapabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noSnormCapabilities, bufferIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
|
||||
// 8-bit signed-normalized storage is core ES, so only EXT_render_snorm gates the 8-bit bit;
|
||||
// the 16-bit one also needs EXT_texture_norm16 for the encoding to exist at all.
|
||||
MG_External::GLESCapabilities noNorm16Capabilities{};
|
||||
noNorm16Capabilities.SupportsRenderSnorm = true;
|
||||
noNorm16Capabilities.SupportsNorm16Texture = false;
|
||||
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
}
|
||||
|
||||
// ---- Signed-normalized colour-renderable substitution (KHR-GL4x.texture_swizzle on Mali) -------
|
||||
//
|
||||
// A driver without GL_EXT_render_snorm treats every signed-normalized format as texture-only, so a
|
||||
// colour attachment in one of them leaves the ES framebuffer incomplete: the draw lands nowhere and
|
||||
// the readback falls through to the CPU shadow, which for a glTexImage2D(..., nullptr) output
|
||||
// texture is all zeroes. The render-target bits used to reach GL_RGB16_SNORM alone, so five of the
|
||||
// eight SNORM formats - and in particular the single-channel GL_R8_SNORM / GL_R16_SNORM that
|
||||
// KHR-GL4x.texture_swizzle renders into for EVERY SNORM source format - had no fallback at all.
|
||||
|
||||
TEST_F(TextureTest, SnormRenderTargetOptionsApplyToEverySignedNormalizedFormat) {
|
||||
using MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions;
|
||||
const Flags<PixelFormatNormalizeOptionBit> requested =
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget | PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
|
||||
for (const GLenum internalFormat : {GL_R8_SNORM, GL_RG8_SNORM, GL_RGB8_SNORM, GL_RGBA8_SNORM}) {
|
||||
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
// The two bits are per precision class, so the 16-bit one never reaches an 8-bit format -
|
||||
// that is what keeps the fallback reason from naming both.
|
||||
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
for (const GLenum internalFormat : {GL_R16_SNORM, GL_RG16_SNORM, GL_RGB16_SNORM, GL_RGBA16_SNORM}) {
|
||||
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
// GL_RGB16_SNORM used to be granted the 16-bit bit only when the three-channel widening was
|
||||
// requested alongside it, which made the answer depend on the order the caller assembled its
|
||||
// option set in. The capability probe and the runtime storage choice assemble different sets.
|
||||
EXPECT_TRUE(GetApplicablePixelFormatNormalizeOptions(GL_RGB16_SNORM,
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
|
||||
// Nothing else responds to either bit; an unsigned-normalized or float format keeps its storage.
|
||||
for (const GLenum internalFormat : {GL_R8, GL_R16, GL_RGBA8, GL_RGBA16, GL_RGB16F, GL_RGBA32F, GL_RGB9_E5}) {
|
||||
EXPECT_FALSE(GetApplicablePixelFormatNormalizeOptions(internalFormat, requested))
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, SnormRenderTargetSubstitutesKeepEveryChannelValueExactly) {
|
||||
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
|
||||
struct Case {
|
||||
GLenum requested;
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
GLenum internalFormat;
|
||||
GLenum format;
|
||||
GLenum type;
|
||||
};
|
||||
const Flags<PixelFormatNormalizeOptionBit> snorm8RT = PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
const Flags<PixelFormatNormalizeOptionBit> snorm16RT = PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
|
||||
const Case cases[] = {
|
||||
// 8-bit: a half float represents every v/127 exactly (the worst case, -123/127, quantizes
|
||||
// 0.03 of a SNORM step away), so it is the same storage GL_RGBA8_SNORM already always got.
|
||||
{GL_R8_SNORM, snorm8RT, GL_R16F, GL_RED, GL_FLOAT},
|
||||
{GL_RG8_SNORM, snorm8RT, GL_RG16F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA8_SNORM, snorm8RT, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit: NOT a half float. Its spacing just below 1.0 is some 16 SNORM steps, so it hands
|
||||
// -23451/32767 back as -23457 against a conformance window of one step; a 32-bit float
|
||||
// round-trips all 65535 channel values.
|
||||
{GL_R16_SNORM, snorm16RT, GL_R32F, GL_RED, GL_FLOAT},
|
||||
{GL_RG16_SNORM, snorm16RT, GL_RG32F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA16_SNORM, snorm16RT, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// The render-target bit outranks the narrower fallbacks, whichever way the caller's option
|
||||
// set was assembled: the capability probe folds the driver options in, the runtime storage
|
||||
// choice can see the render-target bit alone, and the two have to pick the same storage.
|
||||
{GL_R16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16, GL_R32F, GL_RED, GL_FLOAT},
|
||||
{GL_RG16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoSnorm16, GL_RG32F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA16_SNORM,
|
||||
snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16 | PixelFormatNormalizeOptionBit::NoSnorm16,
|
||||
GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// The three-channel formats go on through the widening, which outranks everything.
|
||||
{GL_RGB8_SNORM, snorm8RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA16F, GL_RGBA,
|
||||
GL_FLOAT},
|
||||
{GL_RGB16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA32F, GL_RGBA,
|
||||
GL_FLOAT},
|
||||
// Control: with EXT_render_snorm neither bit is ever set, so the driver that renders to the
|
||||
// signed-normalized encoding keeps storing it byte for byte. This is the shape Adreno and
|
||||
// llvmpipe take, which is why the substitution is invisible on every gate the project runs.
|
||||
{GL_R8_SNORM, PixelFormatNormalizeOptionBit::None, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||
{GL_RG8_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG8_SNORM, GL_RG, GL_BYTE},
|
||||
{GL_R16_SNORM, PixelFormatNormalizeOptionBit::None, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||
{GL_RG16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG16_SNORM, GL_RG, GL_SHORT},
|
||||
{GL_RGBA16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||
// ...and the bit for the other precision class does nothing on its own.
|
||||
{GL_R8_SNORM, snorm16RT, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||
{GL_R16_SNORM, snorm8RT, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||
};
|
||||
|
||||
for (const auto& testCase : cases) {
|
||||
GLenum internalFormat = 0;
|
||||
GLenum format = 0;
|
||||
GLenum type = 0;
|
||||
NormalizePixelFormat(testCase.requested, testCase.options, &internalFormat, &format, &type);
|
||||
EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested;
|
||||
EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested;
|
||||
EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, ThreeChannelRenderTargetOptionAppliesToEveryDeniedThreeChannelFormat) {
|
||||
@@ -3910,9 +4043,10 @@ TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTo
|
||||
{GL_RGB16F, widen, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT},
|
||||
{GL_RGB32F, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit SNORM keeps its encoding where EXT_render_snorm can render to it; a half float's
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly, so the driver that
|
||||
// cannot render to the encoding gets the 32-bit float rather than the half.
|
||||
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
|
||||
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
@@ -4682,6 +4816,208 @@ TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's boundaries, and
|
||||
// this validator had no bounds check whatsoever: the one call shaped like one,
|
||||
// ValidateCopyImageBlockAlignment, returns true on its first line for every UNCOMPRESSED format.
|
||||
// Texture endpoints only looked covered because the ES driver raised its own error - which
|
||||
// DirectGLES logs and swallows, so the application saw GL_NO_ERROR and a destination that never
|
||||
// changed (KHR-GL43.copy_image.exceeding_boundaries).
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsARegionThatLeavesTheImage) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The region that exactly reaches the far edge is the boundary this must NOT reject - a
|
||||
// validator that answered INVALID_VALUE to every non-origin region would satisfy the negatives
|
||||
// below and break every legal partial copy.
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 4, 4, 0, dstTexture, GL_TEXTURE_2D, 0, 4, 4, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// One texel past it on x, on y, and on the destination side.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 5, 4, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 4, 5, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 5, 5, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// A negative origin is out of bounds on the other side of the same rule.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, -1, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The endpoint the missing bounds check actually cost: a renderbuffer never reaches the ES
|
||||
// driver's texture-shaped checks either, so a 4x4 region at y = 14 of a 16x16 renderbuffer - the
|
||||
// exact sub-case KHR-GL43.copy_image.exceeding_boundaries starts with, GL_RENDERBUFFER being first
|
||||
// in its target list - was accepted outright.
|
||||
TEST_F(TextureTest, CopyImageSubDataBoundsARenderbufferRegion) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 16, 16);
|
||||
GLuint renderbuffer = 0;
|
||||
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 16, 16);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 12, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 14, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// ...and as the destination, where the same renderbuffer has the same one image.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 14, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// A renderbuffer has exactly one slice, so any z at all is out of range.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 1, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The z axis was structurally unbounded - srcZ/dstZ did not even reach the validator - so a layer
|
||||
// range running off the end of an array reached the backend as an out-of-range image subresource.
|
||||
TEST_F(TextureTest, CopyImageSubDataBoundsTheLayerRangeOfAnArray) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Layers 5..11 of a 12-layer array: the last one the range may reach.
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 5, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||
0, 0, 0, 5, 4, 4, 7);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 6, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||
0, 0, 0, 0, 4, 4, 7);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||
0, 0, 0, 6, 4, 4, 7);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The convention the bounds check has to get right, and the one that would silently reject legal
|
||||
// copies if it did not: on a CUBE MAP the z axis selects among the six faces, which this frontend
|
||||
// keeps as six separate one-slice upload targets - so the level's own extent reports depth 1 and a
|
||||
// bound taken from it would refuse every whole-cube copy.
|
||||
TEST_F(TextureTest, CopyImageSubDataCountsCubeMapFacesOnTheZAxis) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcTexture);
|
||||
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstTexture);
|
||||
ASSERT_NE(srcObject, nullptr);
|
||||
ASSERT_NE(dstObject, nullptr);
|
||||
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||
GTEST_SKIP() << "this context could not give the cube maps storage";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP, 0, 0, 0, 0, dstTexture, GL_TEXTURE_CUBE_MAP,
|
||||
0, 0, 0, 0, 8, 8, 6);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// A seventh face does not exist.
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP, 0, 0, 0, 1, dstTexture, GL_TEXTURE_CUBE_MAP,
|
||||
0, 0, 0, 0, 8, 8, 6);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The other axis convention: GL puts a 1D ARRAY's layers on y for this entry point (srcY is the
|
||||
// first layer, srcHeight the layer count), which is also where this frontend keeps them - so the
|
||||
// level extent answers directly and z stays a single slice.
|
||||
TEST_F(TextureTest, CopyImageSubDataBoundsA1DArraysLayersOnTheYAxis) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_1D_ARRAY, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_1D_ARRAY, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 16, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 16, 8);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcTexture);
|
||||
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstTexture);
|
||||
ASSERT_NE(srcObject, nullptr);
|
||||
ASSERT_NE(dstObject, nullptr);
|
||||
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||
GTEST_SKIP() << "this context could not give the 1D arrays storage";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 3, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
|
||||
0, 0, 3, 0, 4, 5, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
g_copyImageSubDataCall = {};
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 4, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
|
||||
0, 0, 0, 0, 4, 5, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
|
||||
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
|
||||
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
|
||||
|
||||
Reference in New Issue
Block a user