mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
57
Commits
18fccdd796
...
01d20e5c96
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d20e5c96 | ||
|
|
b04c67d9a8 | ||
|
|
685d83e3ec | ||
|
|
009b140691 | ||
|
|
1f44e5bc1d | ||
|
|
d9aebcba26 | ||
|
|
0db666897e | ||
|
|
5f445e499f | ||
|
|
c52ebd5bf6 | ||
|
|
8e072bc793 | ||
|
|
88ee75be0e | ||
|
|
0dbb4ceba8 | ||
|
|
a94b3e0bd5 | ||
|
|
764b6e044d | ||
|
|
c1d89de729 | ||
|
|
c136384f97 | ||
|
|
1920a3d16f | ||
|
|
11f4b4bd3b | ||
|
|
9ef33f4274 | ||
|
|
e430e1b3be | ||
|
|
e315d9e798 | ||
|
|
6f299372c6 | ||
|
|
be7bf21eb8 | ||
|
|
0e4302b399 | ||
|
|
c9c2dcb42a | ||
|
|
2d938971b9 | ||
|
|
f7e23d5d83 | ||
|
|
02fbb816e9 | ||
|
|
0e0882cfc6 | ||
|
|
de09646d5e | ||
|
|
747864777e | ||
|
|
6fc3504bd9 | ||
|
|
df1bcdba09 | ||
|
|
843c61dee1 | ||
|
|
c0a3f4cc50 | ||
|
|
06744fde7f | ||
|
|
6cc9faf772 | ||
|
|
7168f2ef77 | ||
|
|
07669aacd4 | ||
|
|
d52a3b2196 | ||
|
|
9e23016dd4 | ||
|
|
f1354dc25e | ||
|
|
b7a694711a | ||
|
|
2ae848ca19 | ||
|
|
acf86d1fb6 | ||
|
|
07a0408a28 | ||
|
|
8cf2e2aea9 | ||
|
|
31252cf0da | ||
|
|
2635fe84b6 | ||
|
|
e3163233a5 | ||
|
|
eb9e4fdac1 | ||
|
|
90b7a689c5 | ||
|
|
e69e939d1a | ||
|
|
b675e2a0b0 | ||
|
|
6979926a6f | ||
|
|
d5286e69b6 | ||
|
|
0f2fcbc469 |
Vendored
+1
-1
Submodule 3rdparty/glslang updated: fa562bb911...7e25545174
@@ -389,6 +389,19 @@ namespace MobileGL {
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
// backend that cannot host a cull distance MUST report it. The failure this prevents
|
||||
// is worse than the clip one, because cull distance discards the whole primitive:
|
||||
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
|
||||
// gl_MaxCullDistances from it, SPIRV-Cross then emits
|
||||
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
|
||||
// without that extension rejects the program in an info log nobody surfaces. These
|
||||
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
|
||||
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
|
||||
// is: they describe the no-backend case (standalone compiles, unit tests).
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||
|
||||
@@ -307,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return capabilities.MaxColorTextureSamples;
|
||||
}
|
||||
|
||||
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
|
||||
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
|
||||
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
|
||||
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
|
||||
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
|
||||
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
|
||||
// in the GLES loader) and the walk below would never run past one sample, recording {1}
|
||||
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
|
||||
// reports 4. Once the frontend validates against this list, that would reject every
|
||||
// multisample renderbuffer on such a context.
|
||||
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
|
||||
GLenum imageFormat) {
|
||||
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
|
||||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
|
||||
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
|
||||
}
|
||||
|
||||
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
||||
GLuint texture, TextureInternalFormat format) {
|
||||
GLuint framebuffer = 0;
|
||||
@@ -717,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||
GetRenderbufferFeatureCaps(logicalFormat));
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
||||
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
} else {
|
||||
@@ -731,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||
}
|
||||
const Int maxSamples =
|
||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
|
||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||
}
|
||||
@@ -1465,6 +1482,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
|
||||
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
|
||||
// would reject.
|
||||
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
|
||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
||||
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
||||
|
||||
@@ -351,9 +351,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Only sync up to the high-water mark of app-touched points; the fixed array is 36
|
||||
// Only sync up to the high-water mark of app-touched points; the fixed array is 84
|
||||
// deep but apps bind a handful, so the never-touched tail is already at GL default 0.
|
||||
auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target);
|
||||
// ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5
|
||||
// minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend
|
||||
// index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost
|
||||
// by stopping: this frontend-indexed pass exists for the compute path, and the
|
||||
// per-program rebind in BindCurrentProgramWithResources - which is what actually feeds
|
||||
// a shader - remaps every block a program declares onto a compacted ES point, so a
|
||||
// block bound at GL point 83 still reaches its shader.
|
||||
if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) {
|
||||
bindingPointCnt = std::min(bindingPointCnt,
|
||||
static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
|
||||
}
|
||||
for (SizeT i = 0; i < bindingPointCnt; ++i) {
|
||||
auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i);
|
||||
auto& obj = point.GetBoundObject();
|
||||
@@ -2309,6 +2320,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core)
|
||||
// Both halves are gated on the same entry point rather than on a version check:
|
||||
// GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core /
|
||||
// OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would
|
||||
// only push an INVALID_ENUM into the driver's queue. This is NOT part of the
|
||||
// SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to
|
||||
// put a guard.
|
||||
if (g_GLESFuncs.glMinSampleShading) {
|
||||
if (forceFullPush ||
|
||||
parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) {
|
||||
if (parameters.SampleShadingEnabled) {
|
||||
g_GLESFuncs.glEnable(GL_SAMPLE_SHADING);
|
||||
} else {
|
||||
g_GLESFuncs.glDisable(GL_SAMPLE_SHADING);
|
||||
}
|
||||
}
|
||||
if (forceFullPush || parameters.MinSampleShadingValue !=
|
||||
g_syncedRenderStateParameters.MinSampleShadingValue) {
|
||||
g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_syncedRenderStateVersion = currentRenderStateVersion;
|
||||
// Byte copy, not member copy: it also clones the frontend struct's padding bytes,
|
||||
// which is what lets the span memcmps above answer "unchanged" exactly instead of
|
||||
@@ -2461,9 +2495,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
|
||||
// built program wrong. -1 is "this program needed no such stage", which compares
|
||||
// equal to itself and costs every other program one integer test.
|
||||
//
|
||||
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL are baked into the same stage for the same
|
||||
// reason (ES has neither the state nor an entry point), so glPatchParameterfv
|
||||
// makes it stale too. Both level comparisons sit INSIDE the >= 0 guard: a program
|
||||
// with a control stage of its own - which is nearly all of them - still pays only
|
||||
// the one integer test.
|
||||
//
|
||||
// Compared by BIT PATTERN, matching what DirectVulkan hashes into its module key.
|
||||
// A float compare here would never settle for a NaN level - NaN != NaN - and every
|
||||
// draw of that program would re-transpile, re-compile and re-link a byte-identical
|
||||
// shader. glPatchParameterfv accepts NaN by design.
|
||||
//
|
||||
// The gl_PerVertex MEMBER SET needs no clause of its own here, and that asymmetry
|
||||
// with DirectVulkan is deliberate rather than an omission. It can only change with
|
||||
// the evaluation stage, i.e. across a relink - which the link-version test at the
|
||||
// top of this condition already catches - and this backend never invents the shape
|
||||
// in the first place: AttachPassthroughTessControlStage extracts the member text
|
||||
// out of the neighbouring stages' emitted ESSL on every rebuild
|
||||
// (ExtractPerVertexBlockMembers, "mirrored, never invented"). DirectVulkan needs
|
||||
// the mask in its key precisely because it does NOT mirror - it redeclares from a
|
||||
// member set it has to be told.
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) {
|
||||
(twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
|
||||
MG_State::pGLContext->GetPatchDefaultOuterLevel()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
|
||||
MG_State::pGLContext->GetPatchDefaultInnerLevel())))) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
@@ -3524,6 +3583,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& drawIndirectBuffer,
|
||||
GLsizei drawcount, GLsizei stride, const char* label) {
|
||||
(void)label;
|
||||
// An indirect command's firstIndex/count live in GPU memory, so the substitution has
|
||||
// to rewrite the whole element array buffer rather than this draw's range - which is
|
||||
// exactly what it does when no CPU-known count is handed to it. Held for the whole
|
||||
// command loop so every command in the batch reads the rewritten copy.
|
||||
//
|
||||
// firstIndex counts ELEMENTS, so it survives a widened copy untouched; what does not
|
||||
// survive is the type and the element size, which are re-taken from the substitution
|
||||
// below for both the native and the CPU-unrolled path.
|
||||
const ScopedRestartIndexSubstitution restart(type, /*count=*/0, /*indices=*/nullptr);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
type = restart.IndexType();
|
||||
indexSize = MG_Util::GetGLTypeSize(type);
|
||||
const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
|
||||
if (useNative) {
|
||||
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
|
||||
@@ -3829,29 +3900,308 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed all-ones value). If the app
|
||||
// enabled the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index, hard-fail at this draw with
|
||||
// the reason (a fallback would silently drop restarts and corrupt geometry).
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Arbitrary-index primitive restart
|
||||
//
|
||||
// Desktop GL restarts on whatever index glPrimitiveRestartIndex named; GLES core only
|
||||
// ever restarts on the all-ones value of the index type. When the two agree - which
|
||||
// includes every GL_PRIMITIVE_RESTART_FIXED_INDEX user - the render state push at
|
||||
// SyncRenderState is the whole implementation and nothing here does any work. When they
|
||||
// disagree the index DATA is rewritten into a scratch element array buffer.
|
||||
//
|
||||
// This used to throw instead. A throw here unwinds a C++ exception through the C GL ABI
|
||||
// and takes the process down - the same hazard GL_Texture.cpp and RenderState.cpp
|
||||
// already call out - so an application that merely asked for a legal desktop feature
|
||||
// died rather than got an error.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
struct RestartScratchBuffer {
|
||||
Uint id = 0;
|
||||
SizeT capacity = 0;
|
||||
};
|
||||
|
||||
RestartScratchBuffer g_restartIndices;
|
||||
Vector<Uint8> g_restartStaging;
|
||||
|
||||
// Past this the rewrite would stage and re-upload hundreds of megabytes on EVERY
|
||||
// draw (the copy is not memoised, exactly as on the Vulkan side). Decline instead of
|
||||
// trying: a draw that renders nothing is recoverable, a stall of that size is not.
|
||||
constexpr SizeT kMaxRestartRewriteBytes = SizeT{1} << 26; // 64 MiB
|
||||
|
||||
// The index type one step wider than this one, or 0 when there is none. Widening is how
|
||||
// an all-ones value that is a REAL vertex index keeps its meaning while the all-ones
|
||||
// value of the destination type serves as the restart sentinel: a source that cannot
|
||||
// spell 0xFFFF cannot collide with a 16-bit sentinel, and likewise 8 -> 16.
|
||||
GLenum WiderIndexType(GLenum indexType) {
|
||||
switch (indexType) {
|
||||
case GL_UNSIGNED_BYTE: return GL_UNSIGNED_SHORT;
|
||||
case GL_UNSIGNED_SHORT: return GL_UNSIGNED_INT;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 ReadIndex(const Uint8* source, SizeT i, SizeT indexSize) {
|
||||
switch (indexSize) {
|
||||
case 1: return source[i];
|
||||
case 2: {
|
||||
Uint16 narrow = 0;
|
||||
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
||||
return narrow;
|
||||
}
|
||||
default: {
|
||||
Uint32 wide = 0;
|
||||
std::memcpy(&wide, source + i * 4, sizeof(wide));
|
||||
return wide;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteIndex(Uint8* destination, SizeT i, SizeT indexSize, Uint32 value) {
|
||||
switch (indexSize) {
|
||||
case 1: destination[i] = static_cast<Uint8>(value); break;
|
||||
case 2: {
|
||||
const Uint16 narrow = static_cast<Uint16>(value);
|
||||
std::memcpy(destination + i * 2, &narrow, sizeof(narrow));
|
||||
break;
|
||||
}
|
||||
default: std::memcpy(destination + i * 4, &value, sizeof(value)); break;
|
||||
}
|
||||
}
|
||||
|
||||
// True when any index in the range already holds the type's all-ones value, i.e. when
|
||||
// that value is doing double duty as a real vertex index and so cannot also be the
|
||||
// restart sentinel. Only asked on the rare substitution path.
|
||||
Bool ContainsFixedRestartIndex(const Uint8* source, SizeT indexCount, SizeT indexSize,
|
||||
Uint32 fixedMax) {
|
||||
for (SizeT i = 0; i < indexCount; ++i) {
|
||||
if (ReadIndex(source, i, indexSize) == fixedMax) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copies index data, replacing every occurrence of the application's restart index with
|
||||
// the all-ones value of the DESTINATION type - the only one GLES restarts on. The
|
||||
// destination may be wider than the source, which is what makes the copy lossless: a
|
||||
// source index equal to the source's all-ones value zero-extends to something the wider
|
||||
// sentinel can never equal, so it stays the vertex it was.
|
||||
//
|
||||
// Same width in and out is the degenerate case, used when the source contains no
|
||||
// all-ones index at all (nothing to protect) or when there is no wider type to move to.
|
||||
// In that last case only - a GL_UNSIGNED_INT stream that really does use index
|
||||
// 0xFFFFFFFF while asking to restart on a different one - a legal index has to be
|
||||
// nudged to 0xFFFFFFFE, because 32 bits cannot hold both meanings. The caller logs it;
|
||||
// it is the one input this feature cannot represent.
|
||||
void RewriteRestartIndices(const Uint8* source, SizeT indexCount, SizeT sourceIndexSize,
|
||||
SizeT destinationIndexSize, Uint32 applicationRestartIndex,
|
||||
Uint32 destinationFixedMax, Vector<Uint8>& output) {
|
||||
output.resize(indexCount * destinationIndexSize);
|
||||
for (SizeT i = 0; i < indexCount; ++i) {
|
||||
Uint32 value = ReadIndex(source, i, sourceIndexSize);
|
||||
if (value == applicationRestartIndex) {
|
||||
value = destinationFixedMax;
|
||||
} else if (value == destinationFixedMax) {
|
||||
// Only reachable when no widening was possible; see above.
|
||||
value = destinationFixedMax - 1;
|
||||
}
|
||||
WriteIndex(output.data(), i, destinationIndexSize, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Whole-buffer respecify through the manager-wide staging target, so binding it
|
||||
// disturbs no VAO state. glBufferData orphans the previous store, so the upload
|
||||
// never waits on a draw still reading the old contents out of the same name.
|
||||
Bool UploadRestartScratch(SizeT bytes, const void* data) {
|
||||
if (g_restartIndices.id == 0) {
|
||||
GLuint id = 0;
|
||||
g_GLESFuncs.glGenBuffers(1, &id);
|
||||
if (id == 0) return false;
|
||||
g_restartIndices.id = id;
|
||||
g_restartIndices.capacity = 0;
|
||||
}
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_restartIndices.id);
|
||||
SizeT capacity = g_restartIndices.capacity == 0 ? bytes : g_restartIndices.capacity;
|
||||
while (capacity < bytes) capacity *= 2;
|
||||
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||
GL_STREAM_DRAW);
|
||||
g_restartIndices.capacity = capacity;
|
||||
if (data != nullptr && bytes != 0) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundElementArrayBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
|
||||
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what the
|
||||
// substitution has to put back.
|
||||
Uint BoundElementArrayBufferId() {
|
||||
const auto& ibo = BoundElementArrayBuffer();
|
||||
if (!ibo) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) {
|
||||
if (!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
||||
return RestartSubstitutionKind::None;
|
||||
}
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
||||
if (fixedMax == 0) return RestartSubstitutionKind::None;
|
||||
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
if (restartIndex == fixedMax) return RestartSubstitutionKind::None;
|
||||
// Strictly greater, never truncated. GL 4.6 core 10.3.6 compares the fetched index
|
||||
// zero-extended against the full 32-bit state, so an index this type cannot hold matches
|
||||
// nothing. Truncating instead - glPrimitiveRestartIndex(0x100) over GL_UNSIGNED_BYTE data
|
||||
// becoming "restart on 0" - turns the most common index in any mesh into a restart.
|
||||
if (restartIndex > fixedMax) return RestartSubstitutionKind::SuppressRestart;
|
||||
return RestartSubstitutionKind::RewriteIndices;
|
||||
}
|
||||
|
||||
void OnRestartSubstitutionContextDestroyed() {
|
||||
g_restartIndices = {};
|
||||
g_restartStaging.clear();
|
||||
g_restartStaging.shrink_to_fit();
|
||||
}
|
||||
|
||||
ScopedSuppressedPrimitiveRestart::ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind) {
|
||||
if (kind != RestartSubstitutionKind::SuppressRestart) return;
|
||||
// SyncRenderState turned the driver's fixed-index restart on because GL_PRIMITIVE_RESTART
|
||||
// is enabled; for this draw's index type it would restart on a value the application
|
||||
// never named. Toggled directly rather than through the render-state shadow, and put back
|
||||
// in the destructor, so the shadow stays true and the next draw pays nothing.
|
||||
g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
m_suppressed = true;
|
||||
}
|
||||
|
||||
ScopedSuppressedPrimitiveRestart::~ScopedSuppressedPrimitiveRestart() {
|
||||
if (!m_suppressed) return;
|
||||
g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
}
|
||||
|
||||
ScopedRestartIndexSubstitution::ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count,
|
||||
const void* indices)
|
||||
: m_kind(ResolveRestartSubstitution(indexType)), m_capOverride(m_kind), m_indices(indices),
|
||||
m_indexType(indexType) {
|
||||
if (m_kind != RestartSubstitutionKind::RewriteIndices) {
|
||||
return;
|
||||
}
|
||||
Uint32 fixedMax = 0;
|
||||
switch (indexType) {
|
||||
case GL_UNSIGNED_BYTE: fixedMax = 0xFFu; break;
|
||||
case GL_UNSIGNED_SHORT: fixedMax = 0xFFFFu; break;
|
||||
case GL_UNSIGNED_INT: fixedMax = 0xFFFFFFFFu; break;
|
||||
default: return;
|
||||
const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType);
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType);
|
||||
const Uint32 applicationRestartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
const auto& indexBuffer = BoundElementArrayBuffer();
|
||||
|
||||
const Uint8* source = nullptr;
|
||||
SizeT indexCount = 0;
|
||||
SizeT sourceByteOffset = 0;
|
||||
|
||||
if (indexBuffer) {
|
||||
// The WHOLE buffer is rewritten, not just this draw's range, so that every index
|
||||
// keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be
|
||||
// adjusted from here. It is an ELEMENT index, so it survives widening unchanged.
|
||||
const SizeT sizeBytes = indexBuffer->GetSize();
|
||||
if (sizeBytes < sourceIndexSize) {
|
||||
return; // Nothing to restart on; let the driver see the draw unchanged.
|
||||
}
|
||||
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
if (restartIndex != fixedMax) {
|
||||
THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" + std::to_string(restartIndex) +
|
||||
") is not supported by the GLES backend, which only restarts on the fixed index value (" +
|
||||
std::to_string(fixedMax) +
|
||||
") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX or set glPrimitiveRestartIndex "
|
||||
"to that value.");
|
||||
if (sizeBytes > kMaxRestartRewriteBytes) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element "
|
||||
"array buffer rewritten every draw, which is past the %zu-byte ceiling. Use "
|
||||
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones "
|
||||
"value of the index type.",
|
||||
applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes);
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or a
|
||||
// shader write may have moved past it since the last sync.
|
||||
indexBuffer->SyncPersistentMappedRange();
|
||||
indexBuffer->SyncGpuWrites();
|
||||
source = indexBuffer->MappedData();
|
||||
if (source == nullptr) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
|
||||
"the bound element array buffer and none is available.",
|
||||
applicationRestartIndex);
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
indexCount = sizeBytes / sourceIndexSize;
|
||||
sourceByteOffset = reinterpret_cast<SizeT>(indices);
|
||||
} else {
|
||||
// No element array buffer: `indices` is a client pointer, so only the draw's own
|
||||
// range is readable and an indirect draw has nothing to read at all.
|
||||
if (count <= 0 || indices == nullptr || sourceIndexSize == 0) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs either a bound element "
|
||||
"array buffer or a client index array with a CPU-known count.",
|
||||
applicationRestartIndex);
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
if (static_cast<SizeT>(count) * sourceIndexSize > kMaxRestartRewriteBytes) {
|
||||
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART index rewrite of %zu bytes is past the %zu-byte "
|
||||
"ceiling.",
|
||||
static_cast<SizeT>(count) * sourceIndexSize, kMaxRestartRewriteBytes);
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
source = static_cast<const Uint8*>(indices);
|
||||
indexCount = static_cast<SizeT>(count);
|
||||
}
|
||||
|
||||
// Widen only when the source really does use the all-ones value as a vertex index -
|
||||
// otherwise the sentinel is free and the copy stays the caller's width, which keeps the
|
||||
// common substitution allocation-for-allocation identical to the narrow form.
|
||||
GLenum destinationType = indexType;
|
||||
SizeT destinationIndexSize = sourceIndexSize;
|
||||
if (ContainsFixedRestartIndex(source, indexCount, sourceIndexSize, fixedMax)) {
|
||||
const GLenum wider = WiderIndexType(indexType);
|
||||
// An element-array offset that is not a whole number of indices cannot be rescaled
|
||||
// into the widened copy, so such a draw keeps the narrow (lossy) form.
|
||||
const Bool offsetIsWholeIndices = sourceIndexSize != 0 && (sourceByteOffset % sourceIndexSize) == 0;
|
||||
if (wider != 0 && offsetIsWholeIndices &&
|
||||
indexCount * MG_Util::GetGLTypeSize(wider) <= kMaxRestartRewriteBytes) {
|
||||
destinationType = wider;
|
||||
destinationIndexSize = MG_Util::GetGLTypeSize(wider);
|
||||
} else {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses the "
|
||||
"all-ones index %u: this index type cannot spell both, so every all-ones index is "
|
||||
"drawn as %u instead. Use GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones "
|
||||
"value out of the index data.",
|
||||
applicationRestartIndex, fixedMax, fixedMax - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const Uint32 destinationFixedMax = MG_Util::FixedRestartIndexForGLType(destinationType);
|
||||
RewriteRestartIndices(source, indexCount, sourceIndexSize, destinationIndexSize, applicationRestartIndex,
|
||||
destinationFixedMax, g_restartStaging);
|
||||
|
||||
if (!UploadRestartScratch(g_restartStaging.size(), g_restartStaging.data())) {
|
||||
MGLOG_E_ONCE("Draw skipped: could not allocate the scratch element array buffer for GL_PRIMITIVE_RESTART "
|
||||
"index substitution.");
|
||||
m_valid = false;
|
||||
return;
|
||||
}
|
||||
m_previousBinding = BoundElementArrayBufferId();
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id);
|
||||
m_substituted = true;
|
||||
m_indexType = destinationType;
|
||||
// The rewritten copy starts at byte 0 of the scratch buffer and holds one
|
||||
// destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT
|
||||
// offset (rescaled to the new width) and a client-memory draw reads from the front.
|
||||
m_indices = indexBuffer
|
||||
? reinterpret_cast<const void*>((sourceByteOffset / sourceIndexSize) * destinationIndexSize)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution() {
|
||||
if (!m_substituted) return;
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, m_previousBinding);
|
||||
}
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
@@ -3860,9 +4210,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, count, type, indices);
|
||||
g_GLESFuncs.glDrawElements(mode, count, restart.IndexType(), restart.Indices());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3890,10 +4241,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count, restart.IndexType(), restart.Indices(), basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
@@ -4158,9 +4510,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* indices, GLint basevertex) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, type, indices, basevertex);
|
||||
g_GLESFuncs.glDrawRangeElementsBaseVertex(mode, start, end, count, restart.IndexType(), restart.Indices(),
|
||||
basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
@@ -4168,8 +4523,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer;
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, type, indices);
|
||||
g_GLESFuncs.glDrawRangeElements(mode, start, end, count, restart.IndexType(), restart.Indices());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4191,14 +4548,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertexBaseInstanceEXT(mode, count, restart.IndexType(),
|
||||
restart.Indices(), instancecount,
|
||||
basevertex, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, restart.IndexType(), restart.Indices(),
|
||||
instancecount, basevertex);
|
||||
}
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
@@ -4209,9 +4570,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
SetCurrentBaseVertex(basevertex);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount, basevertex);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseVertex(mode, count, type, restart.Indices(), instancecount,
|
||||
basevertex);
|
||||
});
|
||||
SetCurrentBaseVertex(0);
|
||||
}
|
||||
@@ -4221,13 +4585,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
SetCurrentBaseInstance(baseinstance);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
if (UseNativeBaseInstance()) {
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, type, indices, instancecount,
|
||||
baseinstance);
|
||||
g_GLESFuncs.glDrawElementsInstancedBaseInstanceEXT(mode, count, restart.IndexType(), restart.Indices(),
|
||||
instancecount, baseinstance);
|
||||
} else {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
||||
}
|
||||
});
|
||||
SetCurrentBaseInstance(0);
|
||||
@@ -4236,8 +4602,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
|
||||
PrepareForDraw(syncBit);
|
||||
const ScopedRestartIndexSubstitution restart(type, count, indices);
|
||||
if (!restart.DrawIsValid()) return;
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||
g_GLESFuncs.glDrawElementsInstanced(mode, count, restart.IndexType(), restart.Indices(), instancecount);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9996,6 +10364,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BufferImpl::OnBackendContextDestroyed();
|
||||
XfbImpl::OnBackendContextDestroyed();
|
||||
MultiDrawImpl::OnBackendContextDestroyed();
|
||||
OnRestartSubstitutionContextDestroyed();
|
||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||
ReleasePackedWordScratchTexture();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
|
||||
@@ -4605,12 +4605,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// GL_TEXTURE_BORDER_COLOR needs ES 3.2 or EXT/OES_texture_border_clamp; on a driver
|
||||
// without it every such call is INVALID_ENUM, so the parameter is simply not synced.
|
||||
//
|
||||
// The FORM has to be forwarded along with the value. A border colour set through
|
||||
// glTexParameterIiv/Iuiv is an integer one, and an isampler2D/usampler2D fetch of the
|
||||
// border returns whatever the driver's integer border register holds - so pushing it
|
||||
// through glTexParameterfv handed the driver float 255.0 and the shader read back
|
||||
// 1132396544, the bit pattern of that float. glTexParameterIiv/Iuiv are ES 3.2 core
|
||||
// beside GL_TEXTURE_BORDER_COLOR itself, so they sit behind the same capability gate;
|
||||
// the entry-point null check covers a driver that advertises the extension without them.
|
||||
// The redundancy filter has to look at the AUTHORITATIVE representation, not just the
|
||||
// float one: two integer borders that differ above 2^24 (16777216 and 16777217, say)
|
||||
// collapse onto the same float, so a float-only comparison would skip the second sync and
|
||||
// leave the driver holding the first value forever.
|
||||
const auto borderColorForm = stateTextureObject->GetBorderColorForm();
|
||||
if (!isMultisampleTarget && g_GLESCapabilities.SupportsTextureBorderClamp &&
|
||||
m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
|
||||
(m_cacheBorderColor != stateTextureObject->GetBorderColor() ||
|
||||
m_cacheBorderColorI != stateTextureObject->GetBorderColorI() ||
|
||||
m_cacheBorderColorUI != stateTextureObject->GetBorderColorUI() ||
|
||||
m_cacheBorderColorForm != borderColorForm)) {
|
||||
if (borderColorForm == BorderColorForm::Int && g_GLESFuncs.glTexParameterIiv) {
|
||||
const auto& borderColorI = stateTextureObject->GetBorderColorI();
|
||||
const GLint borderColorArray[4] = {borderColorI.x(), borderColorI.y(), borderColorI.z(),
|
||||
borderColorI.w()};
|
||||
g_GLESFuncs.glTexParameterIiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||
} else if (borderColorForm == BorderColorForm::Uint && g_GLESFuncs.glTexParameterIuiv) {
|
||||
const auto& borderColorUI = stateTextureObject->GetBorderColorUI();
|
||||
const GLuint borderColorArray[4] = {borderColorUI.x(), borderColorUI.y(), borderColorUI.z(),
|
||||
borderColorUI.w()};
|
||||
g_GLESFuncs.glTexParameterIuiv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||
} else {
|
||||
const auto& borderColor = stateTextureObject->GetBorderColor();
|
||||
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
|
||||
const GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(),
|
||||
borderColor.w()};
|
||||
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||
m_cacheBorderColor = borderColor;
|
||||
}
|
||||
m_cacheBorderColor = stateTextureObject->GetBorderColor();
|
||||
m_cacheBorderColorI = stateTextureObject->GetBorderColorI();
|
||||
m_cacheBorderColorUI = stateTextureObject->GetBorderColorUI();
|
||||
m_cacheBorderColorForm = borderColorForm;
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
@@ -6730,6 +6762,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
? MG_State::pGLContext->GetPatchVertices()
|
||||
: 3u;
|
||||
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
|
||||
// PATCH_DEFAULT_{OUTER,INNER}_LEVEL are the same kind of dynamic state and are baked
|
||||
// into the same stage (ES has no such state and no entry point to forward them to), so
|
||||
// they are recorded and compared alongside the patch size - the two move together, as
|
||||
// BuildPassthroughTessControlEssl's contract says.
|
||||
m_passthroughTessControlOuterLevel = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchDefaultOuterLevel()
|
||||
: FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
m_passthroughTessControlInnerLevel = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchDefaultInnerLevel()
|
||||
: FloatVec2(1.0f, 1.0f);
|
||||
|
||||
if (tessEvalShaderIndex < 0 ||
|
||||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
|
||||
@@ -6770,8 +6812,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const String outMembers =
|
||||
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
|
||||
|
||||
const String source =
|
||||
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers);
|
||||
const String source = BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices,
|
||||
inMembers, outMembers,
|
||||
m_passthroughTessControlOuterLevel,
|
||||
m_passthroughTessControlInnerLevel);
|
||||
|
||||
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
|
||||
if (backendShaderId == 0) {
|
||||
@@ -6861,8 +6905,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
|
||||
// Re-established by AttachPassthroughTessControlStage below when this program needs
|
||||
// one; cleared first so a program that stops needing one (a relink that now attaches
|
||||
// a real control stage) does not keep comparing against a stale patch size.
|
||||
// a real control stage) does not keep comparing against a stale patch size. The
|
||||
// default levels are re-established from the same call and gated on the same -1.
|
||||
m_passthroughTessControlPatchVertices = -1;
|
||||
m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
// The same shape again for image FORMATS: what a format-less image declaration
|
||||
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||
@@ -7901,15 +7948,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
|
||||
}
|
||||
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor) {
|
||||
// Same gate as the texture-side border colour above.
|
||||
if (g_GLESCapabilities.SupportsTextureBorderClamp && g_GLESFuncs.glSamplerParameterfv) {
|
||||
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor ||
|
||||
m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI ||
|
||||
m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI ||
|
||||
m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) {
|
||||
// Same gate as the texture-side border colour above, and the same reason for
|
||||
// branching on the form: an integer border colour must reach the driver through
|
||||
// glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern
|
||||
// back instead of the value.
|
||||
if (g_GLESCapabilities.SupportsTextureBorderClamp) {
|
||||
if (samplerParams.borderColorForm == BorderColorForm::Int &&
|
||||
g_GLESFuncs.glSamplerParameterIiv) {
|
||||
const GLint borderColorArray[4] = {
|
||||
samplerParams.borderColorI.x(), samplerParams.borderColorI.y(),
|
||||
samplerParams.borderColorI.z(), samplerParams.borderColorI.w()};
|
||||
g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
|
||||
borderColorArray);
|
||||
} else if (samplerParams.borderColorForm == BorderColorForm::Uint &&
|
||||
g_GLESFuncs.glSamplerParameterIuiv) {
|
||||
const GLuint borderColorArray[4] = {
|
||||
samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(),
|
||||
samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()};
|
||||
g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
|
||||
borderColorArray);
|
||||
} else if (g_GLESFuncs.glSamplerParameterfv) {
|
||||
const GLfloat borderColorArray[4] = {
|
||||
samplerParams.borderColor.x(), samplerParams.borderColor.y(),
|
||||
samplerParams.borderColor.z(), samplerParams.borderColor.w()};
|
||||
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
|
||||
borderColorArray);
|
||||
}
|
||||
}
|
||||
m_cacheSamplerParameters.borderColor = samplerParams.borderColor;
|
||||
m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI;
|
||||
m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI;
|
||||
m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm;
|
||||
}
|
||||
#undef SYNC_SAMPLER_PARAM_IF_CHANGED
|
||||
m_isInitialized = true;
|
||||
|
||||
@@ -102,9 +102,98 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||
// What an indexed draw has to do about primitive restart before it can be issued.
|
||||
//
|
||||
// Desktop GL restarts on an application-chosen index (glPrimitiveRestartIndex under
|
||||
// GL_PRIMITIVE_RESTART); GLES core restarts only on the all-ones value of the index type
|
||||
// (GL_PRIMITIVE_RESTART_FIXED_INDEX), which the render-state push enables for BOTH caps.
|
||||
// That leaves three cases, and the difference between the last two is not cosmetic - one
|
||||
// adds restarts, the other has to take away restarts the driver would otherwise make.
|
||||
enum class RestartSubstitutionKind : Uint8 {
|
||||
// Nothing to do: restart is off, the fixed-index cap is on, or the application's
|
||||
// restart index already IS the type's all-ones value. The overwhelmingly common answer.
|
||||
None,
|
||||
// The application's index is representable in this index type and differs from the
|
||||
// all-ones value: the index DATA has to be rewritten so the driver restarts where the
|
||||
// application asked.
|
||||
RewriteIndices,
|
||||
// The application's index cannot be held by this index type at all. GL 4.6 core 10.3.6
|
||||
// compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX, so no index can match and the draw restarts NOWHERE - but the
|
||||
// render-state push has already enabled the driver's fixed-index restart, so the
|
||||
// all-ones value has to be un-restarted for the duration of the draw.
|
||||
SuppressRestart,
|
||||
};
|
||||
RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType);
|
||||
|
||||
// Turns the driver's fixed-index restart off for one draw and back on afterwards, for the
|
||||
// SuppressRestart case above. Separate from the substitution below because the multi-draw
|
||||
// tiers need it on its own: they rewrite the index stream themselves and only ever need the
|
||||
// cap half. Inert for every other kind, and it never touches the render-state shadow - it
|
||||
// puts the driver back exactly where SyncRenderState left it.
|
||||
class ScopedSuppressedPrimitiveRestart {
|
||||
public:
|
||||
explicit ScopedSuppressedPrimitiveRestart(RestartSubstitutionKind kind);
|
||||
~ScopedSuppressedPrimitiveRestart();
|
||||
ScopedSuppressedPrimitiveRestart(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
ScopedSuppressedPrimitiveRestart& operator=(const ScopedSuppressedPrimitiveRestart&) = delete;
|
||||
|
||||
private:
|
||||
Bool m_suppressed = false;
|
||||
};
|
||||
|
||||
// Swaps in a scratch element array buffer holding a copy of the index data in which the
|
||||
// application's restart index has been replaced by the value GLES restarts on. Inert
|
||||
// (and free) unless ResolveRestartSubstitution asks for it. The swap lives for the
|
||||
// object's lifetime, so it covers every pass of a viewport-routed draw, and the previous
|
||||
// GL_ELEMENT_ARRAY_BUFFER name is restored on destruction - which matters beyond tidiness,
|
||||
// because the VAO twin memoises that it already synced that binding.
|
||||
//
|
||||
// The copy may be WIDER than the source (see IndexType): when the source already contains
|
||||
// the type's all-ones value as an ordinary vertex index, that value cannot double as the
|
||||
// restart sentinel, and widening is the only way to keep both meanings. Callers must
|
||||
// therefore take the index type from this object, not from their own argument.
|
||||
class ScopedRestartIndexSubstitution {
|
||||
public:
|
||||
// count/indices describe the draw's index range when the CPU knows it. Pass
|
||||
// count == 0 for an indirect draw, whose count lives in GPU memory: the whole bound
|
||||
// element array buffer is rewritten instead, so every element keeps its position and
|
||||
// a GPU-resident firstIndex - an ELEMENT index, so it survives widening too - still
|
||||
// addresses the index it named.
|
||||
ScopedRestartIndexSubstitution(GLenum indexType, GLsizei count, const void* indices);
|
||||
~ScopedRestartIndexSubstitution();
|
||||
ScopedRestartIndexSubstitution(const ScopedRestartIndexSubstitution&) = delete;
|
||||
ScopedRestartIndexSubstitution& operator=(const ScopedRestartIndexSubstitution&) = delete;
|
||||
|
||||
// False only when a substitution was needed and could not be made. The draw must
|
||||
// then be skipped: issuing it would let the driver silently drop every restart and
|
||||
// weld the primitives on either side together, which is worse than drawing nothing.
|
||||
Bool DrawIsValid() const { return m_valid; }
|
||||
// The element-array offset (or client pointer) the draw must use. Identical to what
|
||||
// was passed in unless a substitution was made.
|
||||
const void* Indices() const { return m_indices; }
|
||||
// The index type the draw must be issued with. Identical to the constructor's unless
|
||||
// the copy had to be widened to keep an all-ones vertex index distinguishable from the
|
||||
// restart sentinel.
|
||||
GLenum IndexType() const { return m_indexType; }
|
||||
|
||||
private:
|
||||
// Declared before m_capOverride so it is initialised first (members initialise in
|
||||
// declaration order): the whole decision is made once, and both the cap override and the
|
||||
// constructor body read the same answer.
|
||||
RestartSubstitutionKind m_kind = RestartSubstitutionKind::None;
|
||||
ScopedSuppressedPrimitiveRestart m_capOverride;
|
||||
const void* m_indices = nullptr;
|
||||
GLenum m_indexType = 0;
|
||||
Uint m_previousBinding = 0;
|
||||
Bool m_substituted = false;
|
||||
Bool m_valid = true;
|
||||
};
|
||||
|
||||
// Drops the scratch element array buffer the substitution above stages through. Like
|
||||
// MultiDrawImpl's scratch names it is abandoned rather than deleted: the name belongs to
|
||||
// the dead ES context, and deleting it would target whatever its successor handed out.
|
||||
void OnRestartSubstitutionContextDestroyed();
|
||||
// Feed the current program's gl_BaseInstance / gl_DrawID / gl_BaseVertex emulation
|
||||
// uniforms. All are no-ops when the program does not read the corresponding builtin.
|
||||
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||
@@ -961,7 +1050,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint16 m_syncedShapeParamsVersion = 0;
|
||||
SamplerParameters m_cacheSamplerParameters;
|
||||
UintVec2 m_cacheLodRange = {0, 1000};
|
||||
// All three representations plus the form, because none of them alone identifies the
|
||||
// border colour the driver texture is holding: two integer borders can share one float
|
||||
// (anything differing above 2^24), and a Float -> Int transition can leave every number
|
||||
// unchanged while still needing a different driver entry point.
|
||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 m_cacheBorderColorI = {0, 0, 0, 0};
|
||||
UintVec4 m_cacheBorderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm m_cacheBorderColorForm = BorderColorForm::Float;
|
||||
Vec4<TextureSwizzleParam> m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha};
|
||||
// GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a
|
||||
@@ -1447,6 +1543,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
// GL_PATCH_DEFAULT_{OUTER,INNER}_LEVEL the same synthesized stage was built with, for
|
||||
// the same reason: ES has neither the state nor an entry point to forward it to, so
|
||||
// glPatchParameterfv's values are compiled in as literals and a program built with one
|
||||
// set is stale for another. Meaningless (and never read) when the patch-vertices field
|
||||
// above is -1, which is the gate the draw path tests first.
|
||||
const FloatVec4& GetPassthroughTessControlOuterLevel() const {
|
||||
return m_passthroughTessControlOuterLevel;
|
||||
}
|
||||
const FloatVec2& GetPassthroughTessControlInnerLevel() const {
|
||||
return m_passthroughTessControlInnerLevel;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1547,6 +1654,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
// The default tessellation levels baked into that same stage. Only meaningful while
|
||||
// the field above is not -1.
|
||||
FloatVec4 m_passthroughTessControlOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 m_passthroughTessControlInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
||||
|
||||
@@ -29,16 +29,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The all-ones value of an index type, which is what GL restarts on once
|
||||
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||
// restart always restarts here and nowhere else.
|
||||
// The index value this batch restarts on, compared at 32 bits against the zero-extended
|
||||
// source index. Normally the all-ones value of the source type, which is what
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
|
||||
// GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
|
||||
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
|
||||
// driver restarts on.
|
||||
//
|
||||
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
|
||||
// restart index the source type cannot hold simply matches nothing, so returning it
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
default: return 0xFFFFFFFFu;
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
@@ -275,10 +280,20 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer) {
|
||||
Bool hasIndexBuffer, Bool arbitraryRestart) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
|
||||
// only ever restarts on the all-ones value. Every tier but the rebased one hands
|
||||
// the application's own index data to the driver, which would then see no restarts
|
||||
// at all and weld the primitives together. The rebased tier is the one that
|
||||
// REWRITES the stream, and RestartSentinelFor already tells it which value to
|
||||
// translate, so it is the only tier this batch can take.
|
||||
if (arbitraryRestart) {
|
||||
return GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
@@ -488,6 +503,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
|
||||
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
|
||||
// a real vertex index while restarting on a different one is the one shape 32 bits
|
||||
// cannot express - the same corner the single-draw substitution reports.
|
||||
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
|
||||
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
|
||||
"has no wider sentinel to move to.",
|
||||
restartSentinel);
|
||||
}
|
||||
g_indexStaging.resize(total);
|
||||
SizeT cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -852,8 +877,14 @@ void main() {
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (drawcount <= 0 || !count || !indices) return;
|
||||
// State-independent and possibly throwing, so it runs before any GL work.
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
// Read before any GL work, because it decides the tier below: a desktop restart index
|
||||
// the driver does not know about can only be honoured by the tier that rewrites the
|
||||
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
|
||||
// needs no rewrite at all - nothing can match it - but it does need the driver's own
|
||||
// fixed-index restart held off for the batch, which is what the scope below does.
|
||||
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
|
||||
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
|
||||
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
|
||||
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
@@ -889,7 +920,8 @@ void main() {
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer);
|
||||
const GLESMultiDrawMode tier =
|
||||
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -921,8 +953,10 @@ void main() {
|
||||
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive.
|
||||
if (!drawn) {
|
||||
// entry points can receive - except that the base-vertex replay hands the
|
||||
// application's own indices to the driver, which cannot restart on a desktop
|
||||
// restart index, so that batch has only the rewriting floor.
|
||||
if (!drawn && !arbitraryRestart) {
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <regex>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
@@ -836,7 +837,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -866,12 +869,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
for (Uint i = 0; i < 4; ++i) {
|
||||
source += " gl_TessLevelOuter[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
|
||||
}
|
||||
for (Uint i = 0; i < 2; ++i) {
|
||||
source += " gl_TessLevelInner[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
|
||||
}
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -368,11 +368,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
|
||||
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
|
||||
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
|
||||
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
|
||||
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
@@ -382,7 +382,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
|
||||
@@ -1006,6 +1006,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it the limit describes a capacity no shader may use, so report none.
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||
// The cull pair, gated on its own feature. shaderCullDistance is separate from
|
||||
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
|
||||
// gate rather than riding on the clip one.
|
||||
m_dynamicParameters.MaxCullDistances =
|
||||
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
|
||||
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
|
||||
// with only one of the two features must not report a combined capacity that implies the
|
||||
// other, so the gate is "either feature" and the value never drops below what is enabled.
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
|
||||
@@ -201,11 +201,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
@@ -435,6 +439,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -37,11 +37,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
|
||||
// identity and both are hashed. The renderer leaves the enable false unless the
|
||||
// device's sampleRateShading feature was enabled
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
|
||||
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
|
||||
// the levels glPatchParameterfv set are compiled INTO that module and are not a
|
||||
// function of the program or of patchControlPoints - see the note on
|
||||
// passthroughTessControlStage.
|
||||
Uint64 passthroughTessControlKey = 0;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
@@ -87,8 +100,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
|
||||
// of which ComputeHash already mixes in.
|
||||
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
|
||||
// of the default tessellation levels - the first two of which ComputeHash already
|
||||
// mixes in, and the third of which arrives through passthroughTessControlKey above.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <spirv-tools/libspirv.h>
|
||||
@@ -3594,18 +3597,136 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) {
|
||||
Uint64 ProgramFactory::ComputePassthroughTessControlKey(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// A plain 32-byte blob of exactly what the generator reads, hashed once. Deliberately over
|
||||
// the RAW BITS rather than the values: two levels that compare unequal must key apart, and
|
||||
// a NaN level - which glPatchParameterfv accepts - compares unequal to itself.
|
||||
struct Blob {
|
||||
Uint32 patchVertices;
|
||||
Uint32 outerBits[4];
|
||||
Uint32 innerBits[2];
|
||||
Uint32 perVertexMembers;
|
||||
} blob{};
|
||||
blob.patchVertices = patchVertices;
|
||||
for (Uint32 i = 0; i < 4; ++i) blob.outerBits[i] = std::bit_cast<Uint32>(defaultOuterLevel[i]);
|
||||
for (Uint32 i = 0; i < 2; ++i) blob.innerBits[i] = std::bit_cast<Uint32>(defaultInnerLevel[i]);
|
||||
blob.perVertexMembers = perVertexMembers;
|
||||
return XXH64(&blob, sizeof(blob), 0);
|
||||
}
|
||||
|
||||
// The member list a gl_PerVertex redeclaration must spell, derived from the mask. Order is
|
||||
// glslang's declaration order and is load-bearing: a redeclaration whose members are the same
|
||||
// set in a different order is a different block.
|
||||
static String BuildPerVertexMemberDeclarations(Uint32 perVertexMembers) {
|
||||
using Bit = ProgramFactory::PerVertexMemberBit;
|
||||
String members;
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::Position)) members += " vec4 gl_Position;\n";
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::PointSize)) members += " float gl_PointSize;\n";
|
||||
// Sized at one, not left unsized: an unsized built-in array in a redeclared block is
|
||||
// implicitly sized by use, and this stage never indexes either distance array.
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::ClipDistance)) members += " float gl_ClipDistance[1];\n";
|
||||
if (perVertexMembers & static_cast<Uint32>(Bit::CullDistance)) members += " float gl_CullDistance[1];\n";
|
||||
return members;
|
||||
}
|
||||
|
||||
Uint32 ProgramFactory::ReflectPerVertexInputMembers(const Vector<Uint>& spirv) {
|
||||
// Minimal, self-contained SPIR-V walk. SPIRV-Reflect is deliberately NOT used: for an
|
||||
// array of interface blocks it reports built_in == -1 on the block and leaves every
|
||||
// member's built_in at 0 (which is SpvBuiltInPosition), so a member walk through it reads
|
||||
// "Position, Position, Position" - the same trap ReflectPassthroughTessControlNeed
|
||||
// documents. The decorations below are unambiguous.
|
||||
constexpr SizeT kHeaderWords = 5;
|
||||
constexpr Uint32 kOpName = 5;
|
||||
constexpr Uint32 kOpDecorate = 71;
|
||||
constexpr Uint32 kOpMemberDecorate = 72;
|
||||
constexpr Uint32 kOpTypeArray = 28;
|
||||
constexpr Uint32 kOpTypePointer = 32;
|
||||
constexpr Uint32 kOpVariable = 59;
|
||||
constexpr Uint32 kDecorationBlock = 2;
|
||||
constexpr Uint32 kDecorationBuiltIn = 11;
|
||||
constexpr Uint32 kStorageClassInput = 1;
|
||||
constexpr Uint32 kBuiltInPosition = 0;
|
||||
constexpr Uint32 kBuiltInPointSize = 1;
|
||||
constexpr Uint32 kBuiltInClipDistance = 3;
|
||||
constexpr Uint32 kBuiltInCullDistance = 4;
|
||||
(void)kOpName;
|
||||
|
||||
if (spirv.size() <= kHeaderWords) return 0;
|
||||
|
||||
UnorderedMap<Uint32, Uint32> arrayElementType; // array id -> element type id
|
||||
UnorderedMap<Uint32, Pair<Uint32, Uint32>> pointerPointee; // pointer id -> (storage class, pointee)
|
||||
UnorderedMap<Uint32, Uint32> structMembers; // struct id -> PerVertexMemberBit mask
|
||||
std::set<Uint32> blockStructs;
|
||||
Vector<Uint32> inputVariablePointerTypes;
|
||||
|
||||
for (SizeT i = kHeaderWords; i < spirv.size();) {
|
||||
const Uint32 wordCount = spirv[i] >> 16;
|
||||
const Uint32 opcode = spirv[i] & 0xFFFFu;
|
||||
if (wordCount == 0 || i + wordCount > spirv.size()) break;
|
||||
const Uint32* words = &spirv[i];
|
||||
switch (opcode) {
|
||||
case kOpTypeArray:
|
||||
if (wordCount >= 4) arrayElementType[words[1]] = words[2];
|
||||
break;
|
||||
case kOpTypePointer:
|
||||
if (wordCount >= 4) pointerPointee[words[1]] = {words[2], words[3]};
|
||||
break;
|
||||
case kOpVariable:
|
||||
if (wordCount >= 4 && words[3] == kStorageClassInput) inputVariablePointerTypes.push_back(words[1]);
|
||||
break;
|
||||
case kOpDecorate:
|
||||
if (wordCount >= 3 && words[2] == kDecorationBlock) blockStructs.insert(words[1]);
|
||||
break;
|
||||
case kOpMemberDecorate:
|
||||
if (wordCount >= 5 && words[3] == kDecorationBuiltIn) {
|
||||
Uint32 bit = 0;
|
||||
switch (words[4]) {
|
||||
case kBuiltInPosition: bit = static_cast<Uint32>(PerVertexMemberBit::Position); break;
|
||||
case kBuiltInPointSize: bit = static_cast<Uint32>(PerVertexMemberBit::PointSize); break;
|
||||
case kBuiltInClipDistance: bit = static_cast<Uint32>(PerVertexMemberBit::ClipDistance); break;
|
||||
case kBuiltInCullDistance: bit = static_cast<Uint32>(PerVertexMemberBit::CullDistance); break;
|
||||
default: break;
|
||||
}
|
||||
structMembers[words[1]] |= bit;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
i += wordCount;
|
||||
}
|
||||
|
||||
// The one Input variable whose type is an array of a Block-decorated struct IS gl_in;
|
||||
// gl_TessCoord and friends are plain scalars/vectors and never match.
|
||||
for (const Uint32 pointerType : inputVariablePointerTypes) {
|
||||
const auto pointer = pointerPointee.find(pointerType);
|
||||
if (pointer == pointerPointee.end()) continue;
|
||||
const auto array = arrayElementType.find(pointer->second.second);
|
||||
if (array == arrayElementType.end()) continue;
|
||||
if (!blockStructs.contains(array->second)) continue;
|
||||
const auto members = structMembers.find(array->second);
|
||||
if (members == structMembers.end()) continue;
|
||||
return members->second;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no
|
||||
// control shader: "the input patch is passed through unmodified", the output patch has
|
||||
// as many vertices as the input one (PATCH_VERTICES), and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Those two levels default to 1.0 and are baked here as literals because
|
||||
// glPatchParameterfv - their only setter - is not implemented in this frontend (it is a
|
||||
// stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means
|
||||
// making the levels a parameter of this source AND of the cache key in
|
||||
// GetOrCreatePassthroughTessControlStage; the two must move together, so they are named
|
||||
// together here.
|
||||
// Those two levels are baked in as literals - Vulkan has no equivalent dynamic state, so
|
||||
// compiling them in is the only way to honour glPatchParameterfv. That makes them part of
|
||||
// this module's identity: GetOrCreatePassthroughTessControlStage keys its cache on them,
|
||||
// and PipelineFactory hashes them into the pipeline key. The three must move together.
|
||||
//
|
||||
// gl_out carries gl_Position and nothing else on purpose. The evaluation stage that
|
||||
// reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds
|
||||
@@ -3619,60 +3740,91 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// this from having to know the domain.
|
||||
String source = "#version 450 core\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs
|
||||
// carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan
|
||||
// matches built-in interface blocks by their whole shape, and the two obvious spellings
|
||||
// are both wrong:
|
||||
// gl_in and gl_out are redeclared to the exact gl_PerVertex the NEIGHBOURING EVALUATION
|
||||
// STAGE carries, because Vulkan matches built-in interface blocks by their whole shape,
|
||||
// and the two obvious spellings are both wrong:
|
||||
// * narrowing the block to gl_Position alone makes the evaluation stage read a patch of
|
||||
// zeroes (degenerate triangles, nothing rasterized), and
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields FOUR members -
|
||||
// it appends gl_CullDistance - where a linked vertex+evaluation program has three.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a
|
||||
// vertex+evaluation program through this same compiler and fails if the two shapes ever
|
||||
// stop agreeing, rather than letting the mismatch show up as a black frame.
|
||||
// * taking glslang's DEFAULT block for a standalone control stage yields whatever THIS
|
||||
// source's #version implies, which is unrelated to the evaluation stage's.
|
||||
//
|
||||
// The member set is a PARAMETER rather than a constant, and that is the whole point: it
|
||||
// was hardcoded to {gl_Position, gl_PointSize, gl_ClipDistance[1]}, which is the shape a
|
||||
// program carries only below #version 450. glslang appends gl_CullDistance to the block
|
||||
// from 450 upward, so every 450/460 program - and every ESSL program, which the source
|
||||
// processor rewrites to "#version 460 core" - carried FOUR members against this stage's
|
||||
// three and got the black-frame-no-error case described above. The mask comes from
|
||||
// ReflectPerVertexInputMembers, read off the evaluation stage's own SPIR-V.
|
||||
// PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch, and it now
|
||||
// links the program at both 430 and 460.
|
||||
//
|
||||
// Only gl_Position is written. gl_PointSize is declared but left alone deliberately:
|
||||
// writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize
|
||||
// feature, which this renderer does not enable, so a program whose evaluation stage reads
|
||||
// gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap
|
||||
// this trades for not making every tessellated pipeline depend on an optional feature.
|
||||
source += "in gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n"
|
||||
" vec4 gl_Position;\n"
|
||||
" float gl_PointSize;\n"
|
||||
" float gl_ClipDistance[1];\n"
|
||||
"} gl_out[];\n";
|
||||
const String perVertexBody = BuildPerVertexMemberDeclarations(perVertexMembers);
|
||||
source += "in gl_PerVertex {\n" + perVertexBody + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
source += "out gl_PerVertex {\n" + perVertexBody + "} gl_out[];\n";
|
||||
source += "void main() {\n";
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
for (Uint32 i = 0; i < 4; ++i) {
|
||||
source += " gl_TessLevelOuter[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultOuterLevel[i]) + ";\n";
|
||||
}
|
||||
for (Uint32 i = 0; i < 2; ++i) {
|
||||
source += " gl_TessLevelInner[" + std::to_string(i) +
|
||||
"] = " + MG_Util::ShaderTranspiler::TessellationLevelLiteral(defaultInnerLevel[i]) + ";\n";
|
||||
}
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) {
|
||||
VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(
|
||||
Uint32 patchVertices, const FloatVec4& defaultOuterLevel, const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers) {
|
||||
// Everything compiled into the stage, folded into one key. The patch size alone stopped
|
||||
// being enough once glPatchParameterfv could change the levels: two modules that differ
|
||||
// only in a baked-in level are different modules, and pipelines built from either may be
|
||||
// alive at the same time. The gl_PerVertex member set joins it for the same reason - two
|
||||
// programs at different GLSL versions need differently-shaped blocks.
|
||||
const Uint64 key =
|
||||
ComputePassthroughTessControlKey(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
|
||||
// A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a
|
||||
// generator that cannot compile from re-running glslang on every draw.
|
||||
const auto cached = m_passthroughTessControlStages.find(patchVertices);
|
||||
const auto cached = m_passthroughTessControlStages.find(key);
|
||||
if (cached != m_passthroughTessControlStages.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
// The key stopped being bounded when the levels joined it: patchVertices alone could only
|
||||
// take 32 values, but six unclamped application floats can take any number, and an
|
||||
// application that ramps a level per frame would retain one VkShaderModule per frame for
|
||||
// the lifetime of the device. Flushed wholesale rather than aged: a module is not
|
||||
// referenced by the pipelines built from it (Vulkan copies what it needs at
|
||||
// vkCreateGraphicsPipelines), everything here runs on the GL thread, and an application
|
||||
// that can overflow this cap is already recompiling every frame - so the flush costs it
|
||||
// nothing it was not paying anyway.
|
||||
if (m_passthroughTessControlStages.size() >= kMaxPassthroughTessControlStages) {
|
||||
MGLOG_D("ProgramFactory: flushing %zu pass-through tessellation control stages; the application has "
|
||||
"used more than %zu distinct (patch size, default level) combinations",
|
||||
m_passthroughTessControlStages.size(), kMaxPassthroughTessControlStages);
|
||||
for (auto& entry : m_passthroughTessControlStages) {
|
||||
if (entry.second.module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(m_device, entry.second.module, nullptr);
|
||||
}
|
||||
}
|
||||
m_passthroughTessControlStages.clear();
|
||||
}
|
||||
|
||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
|
||||
stage.module = VK_NULL_HANDLE;
|
||||
stage.pName = "main";
|
||||
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = BuildPassthroughTessControlSource(patchVertices);
|
||||
const String source =
|
||||
BuildPassthroughTessControlSource(patchVertices, defaultOuterLevel, defaultInnerLevel, perVertexMembers);
|
||||
// Same compile configuration as every other stage of every other program: this runs on
|
||||
// the GL thread (the draw path), so the live compile env is the right one, and flags=0
|
||||
// is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds).
|
||||
@@ -3686,7 +3838,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for "
|
||||
"patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s",
|
||||
patchVertices, compiled.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3696,7 +3848,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!linked) {
|
||||
MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for "
|
||||
"patchVertices=%u. %s", patchVertices, linked.error().log.c_str());
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3705,7 +3857,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!binary || binary.value().empty() || binary.value().front().empty()) {
|
||||
MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage "
|
||||
"for patchVertices=%u", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3727,14 +3879,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control "
|
||||
"stage for patchVertices=%u", static_cast<Int>(result), patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
stage.module = module;
|
||||
MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u "
|
||||
"(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices);
|
||||
m_passthroughTessControlStages.emplace(patchVertices, stage);
|
||||
m_passthroughTessControlStages.emplace(key, stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -3744,6 +3896,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkProgramObject& entry) const {
|
||||
entry.needsPassthroughTessControl = false;
|
||||
entry.passthroughTessControlEmulatable = false;
|
||||
entry.passthroughPerVertexMembers = 0;
|
||||
|
||||
Bool hasTessEval = false;
|
||||
Bool hasTessControl = false;
|
||||
@@ -3763,6 +3916,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return;
|
||||
const auto& module = spirv[tessEvalModuleIndex];
|
||||
|
||||
// The shape the synthesized control stage has to redeclare. Read here because this is the
|
||||
// only place that holds the evaluation stage's module; a zero mask means the walk found
|
||||
// no input per-vertex block at all, in which case the pre-450 shape is the safe stand-in
|
||||
// (it is what every program carried before gl_CullDistance joined the block).
|
||||
const Uint32 perVertexMembers = ReflectPerVertexInputMembers(module);
|
||||
entry.passthroughPerVertexMembers = perVertexMembers != 0 ? perVertexMembers : kDefaultPerVertexMembers;
|
||||
if (perVertexMembers == 0) {
|
||||
MGLOG_W("ProgramFactory: could not read the evaluation stage's gl_PerVertex block shape; the "
|
||||
"pass-through control stage falls back to the pre-450 three-member form");
|
||||
}
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
|
||||
@@ -76,6 +76,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
// in the order glslang declares them - which is the order a redeclaration must use.
|
||||
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
|
||||
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
|
||||
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
|
||||
enum class PerVertexMemberBit : Uint32 {
|
||||
Position = 1u << 0,
|
||||
PointSize = 1u << 1,
|
||||
ClipDistance = 1u << 2,
|
||||
CullDistance = 1u << 3,
|
||||
};
|
||||
// What a program parsed below #version 450 carries, and the fallback when a module's
|
||||
// block cannot be read.
|
||||
static constexpr Uint32 kDefaultPerVertexMembers =
|
||||
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
|
||||
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
@@ -194,6 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
|
||||
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
|
||||
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
|
||||
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
|
||||
// which the source processor rewrites to "#version 460 core" - carries four members
|
||||
// where a 430 program carries three. A fixed three-member pass-through fed the
|
||||
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
|
||||
// this whole family is written around.
|
||||
Uint32 passthroughPerVertexMembers = 0;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
@@ -249,6 +275,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -267,6 +294,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -311,6 +339,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
@@ -329,6 +358,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -485,18 +515,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size because GL takes the output patch size from PATCH_VERTICES,
|
||||
// which is draw state, not link state - the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The pipeline cache already re-keys on patchControlPoints,
|
||||
// so the module a pipeline was built with is part of that pipeline's identity.
|
||||
// Compiling is bounded by the number of distinct patch sizes a program draws with
|
||||
// (MAX_PATCH_VERTICES = 32 in the worst case, one or two in practice) and only ever
|
||||
// happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices);
|
||||
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
|
||||
// member set, because all three decide what the generator emits. The size comes from
|
||||
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
|
||||
// - draw state rather than link state, and the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
|
||||
// own SPIR-V, so two programs at different GLSL versions need different modules. The
|
||||
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
|
||||
// part of that pipeline's identity. Compiling is bounded by the number of distinct
|
||||
// (size, levels, members) combinations a program draws with - one or two in practice -
|
||||
// and only ever happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices);
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The identity of one such module: everything the generator bakes in, folded into a
|
||||
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
|
||||
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
|
||||
// same value into the pipeline hash so a pipeline can never be handed a module built for
|
||||
// different levels or a different block shape.
|
||||
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
|
||||
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
|
||||
// Input variable that is an array of a Block-decorated struct). Zero when the module has
|
||||
// no such block. Exposed for tests, which is the only way to pin the shape agreement
|
||||
// without a device.
|
||||
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -556,11 +608,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by input patch size. Never evicted: at most
|
||||
// MAX_PATCH_VERTICES entries exist for the lifetime of the device, and every pipeline
|
||||
// ever built from one keeps referencing its module. A failed build is cached as
|
||||
// Pass-through tessellation control stages by the identity of what was compiled into
|
||||
// them - the input patch size and the six default tessellation levels, folded into one
|
||||
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
|
||||
// cannot simply be keyed on the patch size any more). A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
UnorderedMap<Uint32, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
//
|
||||
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
|
||||
// nothing, so an application that recomputes a level per frame mints a new key per frame.
|
||||
// Reaching the cap destroys every module and starts over (see the flush in
|
||||
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
|
||||
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#include "VkClearManager.h"
|
||||
|
||||
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
|
||||
// clear key's layer span has to be the same one the render pass builds its attachment view from.
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentLayerCount(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
|
||||
// the entry - so a layered cube map's glClear reached one face and the other five were lost
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
|
||||
@@ -86,34 +86,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
}
|
||||
|
||||
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the
|
||||
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this
|
||||
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a
|
||||
// geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's
|
||||
// upper layers were never written at all.
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
|
||||
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
|
||||
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
|
||||
|
||||
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
|
||||
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
|
||||
// type is not a legal answer for several of the targets GL can attach, and returning it
|
||||
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
|
||||
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
|
||||
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
|
||||
// or dereferenced - by the driver inside vkCreateFramebuffer.
|
||||
//
|
||||
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
|
||||
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
|
||||
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
|
||||
//
|
||||
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
|
||||
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
|
||||
// the only honest answer would be to decline the attachment - and every driver this has run on,
|
||||
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
|
||||
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
|
||||
static VkImageViewType ResolveAttachmentViewType(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const VkTextureManager::TextureResource& resource) {
|
||||
if (attachment.IsLayered()) {
|
||||
switch (resource.viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
default:
|
||||
return resource.viewType;
|
||||
}
|
||||
}
|
||||
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
|
||||
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||
// symmetry with CUBE_ARRAY.
|
||||
//
|
||||
// 3D belongs in the same list and was missing from it, which is why the "per-slice
|
||||
// attachment view is a 2D view whose array layer is the slice" branch in
|
||||
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
|
||||
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
|
||||
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
|
||||
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return resource.viewType;
|
||||
@@ -334,47 +355,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
|
||||
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
|
||||
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
|
||||
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
|
||||
//
|
||||
// This used to be a hand-maintained second copy of that table, and it was missing exactly
|
||||
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
|
||||
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
|
||||
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
|
||||
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
|
||||
// a size-incompatible vkCmdCopyImage.
|
||||
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -772,7 +764,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
@@ -858,7 +850,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,13 +874,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return activeIt->second;
|
||||
return &activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return it->second;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
@@ -1011,8 +1003,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
|
||||
"%u on FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||
continue;
|
||||
@@ -1100,8 +1096,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
if (textureResource == nullptr) {
|
||||
// SyncTextureResource legitimately declines - an unsupported format,
|
||||
// sample count or image-flag combination, or a vkCreateImage the driver
|
||||
// refused. There is no image to attach, so there is no render pass.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
|
||||
"attachment %u on FBO %u; declining the render pass",
|
||||
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
@@ -1122,8 +1125,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
|
||||
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
|
||||
"render pass",
|
||||
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
desc.samples = attachmentSampleCount;
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
@@ -1216,8 +1226,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
|
||||
MOBILEGL_ASSERT(depthTextureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
|
||||
if (depthTextureResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthTextureResource->layout;
|
||||
depthAttachmentDescription.format = depthTextureResource->format;
|
||||
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
||||
@@ -1229,8 +1243,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
||||
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
MOBILEGL_ASSERT(depthRenderbufferResource,
|
||||
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
|
||||
if (depthRenderbufferResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
trackedDepthLayout = depthRenderbufferResource->layout;
|
||||
depthAttachmentDescription.format = depthRenderbufferResource->format;
|
||||
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
|
||||
@@ -1304,8 +1322,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
|
||||
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1327,6 +1351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(depthRenderbufferResource->view);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1424,8 +1454,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
|
||||
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
|
||||
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
|
||||
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
|
||||
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
|
||||
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
|
||||
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
|
||||
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
|
||||
// line on every draw for the life of the process - a decline caches nothing, so every
|
||||
// later draw to the same framebuffer re-enters this path and fails again.
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
const VkResult renderPassResult =
|
||||
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
|
||||
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
|
||||
"render pass",
|
||||
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
VkFramebufferCreateInfo framebufferCreateInfo;
|
||||
@@ -1438,8 +1485,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
framebufferCreateInfo.width = width;
|
||||
framebufferCreateInfo.height = height;
|
||||
framebufferCreateInfo.layers = framebufferLayers;
|
||||
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer));
|
||||
const VkResult framebufferResult =
|
||||
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
|
||||
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
|
||||
// The render pass has no entry to own it yet, so it is destroyed here rather than
|
||||
// leaked - RenderPassEntry's destructor is the only other thing that would.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
|
||||
"%u attachments, %u layers); declining the render pass",
|
||||
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
|
||||
fbo.GetExternalIndex(), width, height,
|
||||
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
|
||||
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
IntVec2 extent = {width, height};
|
||||
RenderPassEntry renderPassEntry {
|
||||
hash,
|
||||
@@ -1464,7 +1524,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
return insertedIt->second;
|
||||
return &insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
|
||||
@@ -243,7 +243,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
|
||||
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
}
|
||||
|
||||
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
|
||||
// family and an integer family and requires the sampler's choice to match the image view's
|
||||
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
|
||||
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
|
||||
enum class BorderColorDomain {
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger
|
||||
};
|
||||
|
||||
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return BorderColorDomain::SignedInteger;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return BorderColorDomain::UnsignedInteger;
|
||||
default:
|
||||
return BorderColorDomain::Float;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
|
||||
// format in which texture components are stored. For signed and unsigned normalized
|
||||
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
|
||||
// floating-point and integer formats, border values are clamped to the representable range of
|
||||
// the format." Every clause of that sentence is a real case here - the clamp is not just the
|
||||
// normalized one.
|
||||
//
|
||||
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
|
||||
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
|
||||
// so a negative border on them must come back as 0 rather than as a negative number the
|
||||
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
|
||||
struct FloatBorderRange {
|
||||
Bool clamped = true;
|
||||
Float minValue = 0.0f;
|
||||
Float maxValue = 1.0f;
|
||||
};
|
||||
|
||||
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
return {false, 0.0f, 0.0f};
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
return {true, -65504.0f, 65504.0f};
|
||||
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
|
||||
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
|
||||
// that matters for correctness is the lower one, and a single conservative upper bound
|
||||
// costs nothing a real border colour will ever notice.
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
return {true, 0.0f, 64512.0f};
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
return {true, 0.0f, 65408.0f};
|
||||
default:
|
||||
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Per-component representable range of an integer texture format, as Int64 so that the whole
|
||||
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
|
||||
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
|
||||
// whose alpha is narrower than its colour channels.
|
||||
struct IntegerBorderRange {
|
||||
Int64 rgbMin = 0;
|
||||
Int64 rgbMax = 0;
|
||||
Int64 alphaMin = 0;
|
||||
Int64 alphaMax = 0;
|
||||
};
|
||||
|
||||
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
|
||||
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
return uniform(-128, 127);
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
return uniform(-32768, 32767);
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
return uniform(0, 255);
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
return uniform(0, 65535);
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
return uniform(0, 4294967295LL);
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return {0, 1023, 0, 3};
|
||||
default:
|
||||
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
|
||||
// clamp that cannot alter a value the GL entry points could have carried.
|
||||
return uniform(-2147483648LL, 2147483647LL);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsDepthTextureFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
m_customBorderColorSupported = initInfo.customBorderColorSupported;
|
||||
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
m_customBorderColorSupported = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.borderColor = borderColor.color;
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
|
||||
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
|
||||
// customBorderColorWithoutFormat was required alongside customBorderColors at device
|
||||
// creation - a GL sampler object has no idea which texture it will be paired with.
|
||||
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
|
||||
if (borderColor.isCustom) {
|
||||
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
|
||||
customBorderColorInfo.customBorderColor = borderColor.customValue;
|
||||
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
|
||||
customBorderColorInfo.pNext = samplerInfo.pNext;
|
||||
samplerInfo.pNext = &customBorderColorInfo;
|
||||
}
|
||||
|
||||
VkSampler vkSampler = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
|
||||
|
||||
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.usesCustomBorderColor = borderColor.isCustom;
|
||||
if (entry.usesCustomBorderColor) {
|
||||
++m_customBorderColorSamplerCount;
|
||||
}
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
ResolvedBorderColor resolved{};
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
}
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
const auto& borderColor = sampler.GetBorderColor();
|
||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||
const auto format = texture.GetFormat();
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
|
||||
if (isDepthTexture) {
|
||||
if (domain != BorderColorDomain::Float) {
|
||||
// An integer image view REQUIRES an integer border colour, whatever the value is - even
|
||||
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
|
||||
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
|
||||
// representation (a plain cast) is as good an answer as any.
|
||||
//
|
||||
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
|
||||
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
|
||||
//
|
||||
// Which representation to start from is the TEXTURE's domain, not the entry-point form
|
||||
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
|
||||
// an integer internal data type and does not define a sign conversion between the two
|
||||
// integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
|
||||
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
|
||||
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
|
||||
// the representation by the FORM instead would answer 0 here, which is a defensible
|
||||
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
|
||||
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
|
||||
// agreement decides it.
|
||||
const auto range = ResolveIntegerBorderRange(format);
|
||||
const auto& borderColorI = sampler.GetBorderColorI();
|
||||
const auto& borderColorUI = sampler.GetBorderColorUI();
|
||||
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
|
||||
Int64 clamped[4];
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
|
||||
: static_cast<Int64>(borderColorI[channel]);
|
||||
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
|
||||
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
|
||||
clamped[channel] = std::clamp(raw, low, high);
|
||||
}
|
||||
|
||||
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
|
||||
// the palette entry it clamps to rather than missing every one of them.
|
||||
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
|
||||
if (allZeroRgb && clamped[3] == 0) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (allZeroRgb && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (domain == BorderColorDomain::UnsignedInteger) {
|
||||
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
|
||||
} else {
|
||||
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
// No custom colour available: pick the nearest of the three integer palette entries
|
||||
// rather than always answering transparent black, which is what turned an integer border
|
||||
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
|
||||
const Bool opaque = clamped[3] != 0;
|
||||
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
|
||||
// format, so it is clamped to that format's representable range first. Without the clamp the
|
||||
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
|
||||
// and fell through to transparent black - every border texel sampled 0 where the test wanted
|
||||
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
|
||||
// 32-bit float formats are unclamped.
|
||||
FloatVec4 borderColor = sampler.GetBorderColor();
|
||||
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
|
||||
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.y(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.z(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.w(), range.minValue, range.maxValue));
|
||||
}
|
||||
|
||||
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
|
||||
if (IsDepthTextureFormat(format)) {
|
||||
if (NearlyEqual(borderColor.x(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
|
||||
NearlyEqual(borderColor.z(), 0.0f);
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
|
||||
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
resolved.customValue.float32[0] = borderColor.x();
|
||||
resolved.customValue.float32[1] = borderColor.y();
|
||||
resolved.customValue.float32[2] = borderColor.z();
|
||||
resolved.customValue.float32[3] = borderColor.w();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Nearest of the three float palette entries. Transparent black stays the answer for a
|
||||
// transparent border, which is what the old unconditional fallback got right by accident.
|
||||
const Bool opaque = borderColor.w() >= 0.5f;
|
||||
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -28,6 +28,13 @@ public:
|
||||
Bool samplerAnisotropySupported = false;
|
||||
// VkPhysicalDeviceLimits::maxSamplerAnisotropy.
|
||||
Float maxSamplerAnisotropy = 1.0f;
|
||||
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
|
||||
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
|
||||
Bool customBorderColorSupported = false;
|
||||
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
|
||||
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
|
||||
// and falls back to the snapped predefined value once it is reached.
|
||||
Uint32 maxCustomBorderColorSamplers = 0;
|
||||
};
|
||||
|
||||
Bool Initialize(const InitInfo& initInfo);
|
||||
@@ -52,6 +59,21 @@ public:
|
||||
// boundaries.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
|
||||
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
|
||||
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
|
||||
//
|
||||
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
|
||||
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
|
||||
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
|
||||
// custom colour was affordable is decided before the key is built, not twice with a budget
|
||||
// change in between.
|
||||
struct ResolvedBorderColor {
|
||||
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
VkClearColorValue customValue{};
|
||||
Bool isCustom = false;
|
||||
};
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
VkSampler handle = VK_NULL_HANDLE;
|
||||
@@ -60,17 +82,18 @@ private:
|
||||
// Frame boundary of the last cache hit; entries idle past the
|
||||
// OnFrameBoundary retirement age have their VkSampler destroyed.
|
||||
Uint64 lastUsedFrameBoundary = 0;
|
||||
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
|
||||
Bool usesCustomBorderColor = false;
|
||||
};
|
||||
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const;
|
||||
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
|
||||
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
|
||||
static VkFilter ToVkFilter(SamplerFilterMode mode);
|
||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) const;
|
||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
|
||||
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
|
||||
@@ -82,6 +105,12 @@ private:
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
Bool m_samplerAnisotropySupported = false;
|
||||
Float m_maxSamplerAnisotropy = 1.0f;
|
||||
Bool m_customBorderColorSupported = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
|
||||
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
|
||||
// and Shutdown.
|
||||
Uint32 m_customBorderColorSamplerCount = 0;
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
|
||||
@@ -46,13 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
struct TextureShapeInfo {
|
||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
@@ -380,7 +373,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
@@ -921,18 +914,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (mipLevel >= resource->mipLevels) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
|
||||
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
|
||||
// SyncTextureResource asks for and may have had refused per format.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so an attachment
|
||||
// view over it addresses SLICES through baseArrayLayer/layerCount: one slice for a
|
||||
// non-layered attachment (a 2D view) and the whole span for a layered one (a 2D_ARRAY view,
|
||||
// which is what a layered GL_TEXTURE_3D attachment plus a gl_Layer-writing geometry shader
|
||||
// means). BOTH spellings are legal only on a 2D-array-compatible image
|
||||
// (VUID-VkImageViewCreateInfo-image-04970 / -06723), which SyncTextureResource asks for and
|
||||
// may have had refused per format.
|
||||
//
|
||||
// The span is validated against the MIP's slice count, never against arrayLayers: a 3D
|
||||
// image's arrayLayers is 1 by construction, so measuring a layered span against it rejected
|
||||
// every layered 3D attachment - the null view that used to reach vkCreateFramebuffer.
|
||||
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D &&
|
||||
(viewType == VK_IMAGE_VIEW_TYPE_2D || viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY)) {
|
||||
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
|
||||
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
|
||||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
|
||||
"2D-array-compatible=%d)",
|
||||
// Not an error line: the render-pass builder turns the null view into one
|
||||
// MGLOG_E_ONCE and a skipped draw, which is the level this belongs at.
|
||||
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d as viewType=%d (mip %u has %u "
|
||||
"slices, 2D-array-compatible=%d)",
|
||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||
mipLevel, sliceCount,
|
||||
static_cast<Int>(viewType), mipLevel, sliceCount,
|
||||
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
@@ -2173,12 +2176,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
|
||||
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||
// Losing 2D-array compatibility only costs framebuffer attachment of this format's
|
||||
// 3D images - per-slice AND layered, since both are spelled as a 2D-family view over
|
||||
// the z axis; failing creation would lose the texture entirely. Recorded here (the
|
||||
// per-format set below) so later syncs neither reprobe nor flag-mismatch against this
|
||||
// image and recreate it, and so GetOrCreateAttachmentViewAtMipLevel declines rather
|
||||
// than handing back a view that cannot exist - the render-pass builder then turns
|
||||
// that decline into a skipped draw instead of a null VkImageView in pAttachments.
|
||||
MGLOG_W_ONCE("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||
"unavailable for it)",
|
||||
"textureId=%d; creating without it (per-slice and layered framebuffer "
|
||||
"attachment of 3D textures in this format will be unavailable)",
|
||||
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||
m_2dArrayCompatibleUnsupported.insert(format);
|
||||
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -22,6 +24,31 @@ class ITextureObject;
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
|
||||
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
|
||||
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
|
||||
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
|
||||
// because Vulkan devices rarely support the 3-channel layouts.
|
||||
//
|
||||
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
|
||||
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
|
||||
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
|
||||
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
|
||||
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
|
||||
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
|
||||
struct TextureFormatInfo {
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
|
||||
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
|
||||
Bool expandRgbToRgba = false;
|
||||
Uint32 componentByteCount = 0;
|
||||
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
|
||||
// `.format` and ignore the rest.
|
||||
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
|
||||
|
||||
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
||||
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
||||
@@ -41,6 +68,37 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
|
||||
return glTexelSize;
|
||||
}
|
||||
|
||||
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
|
||||
//
|
||||
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
|
||||
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
|
||||
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
|
||||
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
|
||||
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
|
||||
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
|
||||
// depending only on which path happens to consume it first - and the materialise path then POPS
|
||||
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
|
||||
// that split gets introduced; keep them the same function.
|
||||
//
|
||||
// Two shapes make this more than `size.z()`:
|
||||
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
|
||||
// just above), so z reads 1 and every layer above the first was silently dropped.
|
||||
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
|
||||
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
|
||||
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
|
||||
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
|
||||
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (!attachment.IsLayered()) {
|
||||
return 1u;
|
||||
}
|
||||
const auto& texture = attachment.GetTexture();
|
||||
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
|
||||
if (target == TextureTarget::TextureCubeMap) {
|
||||
return 6u;
|
||||
}
|
||||
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
|
||||
}
|
||||
|
||||
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
|
||||
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
|
||||
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "MG_State/GLState/SamplerState/SamplerObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject.h"
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_Impl/GLImpl/Texture/GL_Texture.h"
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
// Only reached from an MGLOG_W, which the shipping INFO log level compiles out - so the
|
||||
// missing include never broke a default build and did break every WARN/DEBUG-level one.
|
||||
@@ -30,6 +31,7 @@
|
||||
#include "MG_Util/Texture/PixelStoreProcessor.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
@@ -1417,6 +1419,48 @@ void main() {
|
||||
return {width, height, depth};
|
||||
}
|
||||
|
||||
// How many components of a GL-space texel size actually halve down the mip chain. An array
|
||||
// texture's LAYER count is not a dimension of the image (GL 4.6 core 8.14.3): it stays put
|
||||
// all the way down, and GetMipmapTexelSize parks it in the slot after the image's own
|
||||
// dimensions. This is the same split IsMipmapCompleteForFilter applies, and the two have to
|
||||
// agree - allocating a chain whose layer count shrinks builds levels the completeness rule
|
||||
// then rejects. Vulkan-space extents need none of this: layers live in arrayLayers there,
|
||||
// so resource->depth is already 1 for every array target.
|
||||
static Int MipShrinkingComponentCount(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1DArray:
|
||||
return 1;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
return 2;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
static IntVec3 ComputeMipTexelSizeWithFixedComponents(const IntVec3& baseTexelSize, Uint32 relativeMipLevel,
|
||||
Int shrinkingComponents) {
|
||||
IntVec3 size = baseTexelSize;
|
||||
for (Int component = 0; component < shrinkingComponents && component < 3; ++component) {
|
||||
size[component] = std::max<Int>(size[component] >> static_cast<Int>(relativeMipLevel), 1);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
static Uint32 ComputeFullMipLevelCountWithFixedComponents(const IntVec3& baseTexelSize,
|
||||
Int shrinkingComponents) {
|
||||
Int maxDimension = 1;
|
||||
for (Int component = 0; component < shrinkingComponents && component < 3; ++component) {
|
||||
maxDimension = std::max<Int>(maxDimension, baseTexelSize[component]);
|
||||
}
|
||||
Uint32 mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
++mipLevelCount;
|
||||
}
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture,
|
||||
Uint32 baseMipLevel) {
|
||||
const Uint32 existingMipLevelCount = static_cast<Uint32>(texture.GetMipmapLevelCount());
|
||||
@@ -1429,6 +1473,8 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int shrinkingComponents = MipShrinkingComponentCount(texture.GetTarget());
|
||||
|
||||
for (const auto uploadTarget : uploadTargets) {
|
||||
const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel);
|
||||
const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel);
|
||||
@@ -1445,13 +1491,15 @@ void main() {
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize);
|
||||
const Uint32 requiredMipLevelCount =
|
||||
baseMipLevel + ComputeFullMipLevelCountWithFixedComponents(baseTexelSize, shrinkingComponents);
|
||||
if (existingMipLevelCount >= requiredMipLevelCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (Uint32 level = existingMipLevelCount; level < requiredMipLevelCount; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipTexelSize(baseTexelSize, level - baseMipLevel);
|
||||
const IntVec3 levelTexelSize = ComputeMipTexelSizeWithFixedComponents(
|
||||
baseTexelSize, level - baseMipLevel, shrinkingComponents);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
@@ -3126,7 +3174,9 @@ void main() {
|
||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed.");
|
||||
succeeded = m_samplerManager->Initialize({m_device, &m_config, m_samplerAnisotropyFeatureEnabled,
|
||||
m_physicalDevice.properties.limits.maxSamplerAnisotropy});
|
||||
m_physicalDevice.properties.limits.maxSamplerAnisotropy,
|
||||
m_customBorderColorFeatureEnabled,
|
||||
m_maxCustomBorderColorSamplers});
|
||||
MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed.");
|
||||
succeeded = InitializeBlitResources();
|
||||
MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed.");
|
||||
@@ -3915,10 +3965,15 @@ void main() {
|
||||
// Copies index data, replacing every occurrence of the application's arbitrary restart
|
||||
// index with the fixed all-ones value of the index type - the only one Vulkan restarts
|
||||
// on. An index that already equals the fixed value would then be indistinguishable from
|
||||
// a restart, so it is nudged to the next-lowest value: it can only be a real index (the
|
||||
// application's restart index is a different number), and the vertex it selects is
|
||||
// outside any well-defined draw anyway, whereas leaving it alone would tear the
|
||||
// primitive in two.
|
||||
// a restart, so it is nudged to the next-lowest value, which silently draws the wrong
|
||||
// vertex. That is a real (if narrow) loss and it is reported once rather than left
|
||||
// invisible; DirectGLES avoids it for 8- and 16-bit indices by widening the copy instead,
|
||||
// and the same treatment here is follow-up work.
|
||||
//
|
||||
// The caller guarantees applicationRestartIndex fits the index type, so no truncating
|
||||
// cast is needed - and none may be used: truncating turns glPrimitiveRestartIndex(0x100)
|
||||
// over 8-bit indices into "restart on index 0", which shreds every primitive that
|
||||
// references vertex 0.
|
||||
void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType,
|
||||
Uint32 applicationRestartIndex, Vector<Uint8>& output) {
|
||||
output.resize(sizeBytes);
|
||||
@@ -3932,6 +3987,12 @@ void main() {
|
||||
if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) {
|
||||
indices[i] = fixedMax;
|
||||
} else if (indices[i] == fixedMax) {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over index data that also uses "
|
||||
"the all-ones index %u: both cannot be spelled at this index width, so every "
|
||||
"all-ones index is drawn one vertex lower. Use "
|
||||
"GL_PRIMITIVE_RESTART_FIXED_INDEX, or keep the all-ones value out of the "
|
||||
"index data.",
|
||||
applicationRestartIndex, static_cast<Uint32>(fixedMax));
|
||||
indices[i] = fixedMax - 1;
|
||||
}
|
||||
}
|
||||
@@ -3984,14 +4045,13 @@ void main() {
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartEnabled && !rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
const Uint32 restartIndex = rsp.PrimitiveRestartIndex;
|
||||
Uint32 fixedMax = 0;
|
||||
switch (vkIndexType) {
|
||||
case VK_INDEX_TYPE_UINT8: fixedMax = 0xFFu; break;
|
||||
case VK_INDEX_TYPE_UINT16: fixedMax = 0xFFFFu; break;
|
||||
case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break;
|
||||
default: break;
|
||||
}
|
||||
substituteRestart = restartIndex != fixedMax;
|
||||
const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
|
||||
// STRICTLY less, and never truncated. Equal needs no rewrite (the driver already
|
||||
// restarts there); GREATER means the index type cannot hold the application's restart
|
||||
// index, so GL 4.6 core 10.3.6 says nothing matches it and the draw restarts nowhere -
|
||||
// which is exactly what ResolvePrimitiveRestartEnable told the pipeline, so rewriting
|
||||
// here would put restarts into a stream the pipeline was built not to restart on.
|
||||
substituteRestart = restartIndex < fixedMax;
|
||||
substituteRestartIndex = restartIndex;
|
||||
}
|
||||
|
||||
@@ -4707,8 +4767,9 @@ void main() {
|
||||
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
|
||||
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
|
||||
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
|
||||
// PrimitiveRestart(+FixedIndex), plus the depth write mask
|
||||
// - patch vertices, polygon mode, cull face mode, depth func, logic op
|
||||
// PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask
|
||||
// - patch vertices, polygon mode, cull face mode, depth func, logic op,
|
||||
// min sample shading
|
||||
// - front/back stencil ops + compare funcs (ref/mask are dynamic state)
|
||||
// - per draw buffer up to the render pass's colour span: indexed blend enable,
|
||||
// blend factors/equations, indexed colour write mask (broadcast from index 0
|
||||
@@ -4733,8 +4794,31 @@ void main() {
|
||||
capabilityBits |= p.PrimitiveRestartEnabled ? 1ull << 6 : 0;
|
||||
capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0;
|
||||
capabilityBits |= p.DepthMask ? 1ull << 8 : 0;
|
||||
capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0;
|
||||
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
|
||||
// glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than
|
||||
// versions, so an unhashed float would let a pipeline built at one rate be handed back
|
||||
// after glMinSampleShading moved it - the memo would see identical state.
|
||||
{
|
||||
Uint32 minSampleShadingBits = 0;
|
||||
std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(minSampleShadingBits));
|
||||
}
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PatchVertices));
|
||||
// The default tessellation levels belong here for the same reason PatchVertices does:
|
||||
// when a program has an evaluation stage and no control stage, both are compiled into the
|
||||
// synthesized pass-through control stage, so two draws that differ only in a level need
|
||||
// different pipelines. Hashed over the RAW BITS so a NaN level - which glPatchParameterfv
|
||||
// accepts - keys to itself. Six extra words on a path that only recomputes when the
|
||||
// pipeline-state version moved.
|
||||
for (Uint32 i = 0; i < 4; ++i) {
|
||||
hash = CombinePipelineStateWord(hash,
|
||||
static_cast<Uint64>(std::bit_cast<Uint32>(p.PatchDefaultOuterLevel[i])));
|
||||
}
|
||||
for (Uint32 i = 0; i < 2; ++i) {
|
||||
hash = CombinePipelineStateWord(hash,
|
||||
static_cast<Uint64>(std::bit_cast<Uint32>(p.PatchDefaultInnerLevel[i])));
|
||||
}
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.PolygonModeFront));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.CullFaceModeSetting));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(p.DepthFunc));
|
||||
@@ -4779,13 +4863,42 @@ void main() {
|
||||
return program.HasLinkedShaderStage(ShaderStage::Geometry);
|
||||
}
|
||||
|
||||
// GL primitive restart is defined on the INDEX STREAM (GL 4.6 core 10.3.6): it splits
|
||||
// primitives when a fetched index matches PRIMITIVE_RESTART_INDEX. Two consequences the
|
||||
// capability bits alone cannot express, both resolved here because only the caller knows them:
|
||||
//
|
||||
// - A non-indexed draw has no index stream, so restart is a no-op for it. Leaving the
|
||||
// pipeline's primitiveRestartEnable on for a glDrawArrays is what made the list-topology
|
||||
// guard below refuse those draws, so an application that enables GL_PRIMITIVE_RESTART once
|
||||
// at init lost every glDrawArrays on a device without the extension.
|
||||
// - The comparison is against the full 32-bit restart index with the fetched index
|
||||
// zero-extended, so a restart index the type cannot hold (0x100FF against UNSIGNED_BYTE
|
||||
// data) matches no index and that draw restarts NOWHERE. UploadAndBindIndexBuffer makes the
|
||||
// same call for the rewrite, and the two must agree or the pipeline says "restart" over
|
||||
// index data nothing rewrote.
|
||||
Bool VulkanRenderer::ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
||||
const IndexBufferView* pIndexBufferView) const {
|
||||
if (!(aspects & DrawSetupAspect::IndexBuffer) || pIndexBufferView == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (!rsp.PrimitiveRestartEnabled) {
|
||||
return false;
|
||||
}
|
||||
return rsp.PrimitiveRestartIndex <= MG_Util::FixedRestartIndexForGLType(pIndexBufferView->indexType);
|
||||
}
|
||||
|
||||
VkPipeline VulkanRenderer::GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const RenderPassEntry& renderPassEntry) {
|
||||
const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable) {
|
||||
Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip;
|
||||
if (programObj.stages.empty()) {
|
||||
MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages");
|
||||
@@ -4827,6 +4940,7 @@ void main() {
|
||||
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
||||
entry.renderPassHash == renderPassHash &&
|
||||
entry.pipelineStateHash == pipelineStateHash &&
|
||||
entry.primitiveRestartEnable == primitiveRestartEnable &&
|
||||
entry.transformFlags == transformFlags) {
|
||||
return entry.pipeline;
|
||||
}
|
||||
@@ -5025,22 +5139,50 @@ void main() {
|
||||
: VK_POLYGON_MODE_FILL;
|
||||
|
||||
const VkPrimitiveTopology vkTopology = MG_Util::ConvertPrimitiveModeToVkEnum(mode);
|
||||
const Bool primitiveRestartEnabled =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
// Resolved by the caller (ResolvePrimitiveRestartEnable), which knows whether the draw is
|
||||
// indexed and with what index type; the capability bits alone answer neither.
|
||||
Bool primitiveRestartEnabled = primitiveRestartEnable;
|
||||
|
||||
// GL applies restart to PATCHES only when PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED is true
|
||||
// (GL 4.6 core 10.3.6). MobileGL supports no such thing - neither backend has a way to
|
||||
// restart a patch stream - and GL_FALSE is a legal answer to that query, so a patch draw
|
||||
// simply never restarts here. Doing this BEFORE the feature guard below is what keeps a
|
||||
// perfectly ordinary GL_PATCHES draw from being refused on a device that lacks
|
||||
// VK_EXT_primitive_topology_list_restart. (When GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED
|
||||
// is eventually added to glGetIntegerv it has to report GL_FALSE to stay consistent with
|
||||
// this.)
|
||||
if (vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
|
||||
primitiveRestartEnabled = false;
|
||||
}
|
||||
|
||||
// Primitive restart on a *list* topology requires the primitiveTopologyListRestart feature;
|
||||
// strip/fan restart works without it. Silently dropping restarts would corrupt geometry, so
|
||||
// hard-fail here (at the draw) with the reason when the device lacks the feature.
|
||||
// strip/fan restart works without it. There is no fallback - silently dropping the restarts
|
||||
// would weld the primitives on either side of each one together - so the draw is declined
|
||||
// here with the reason.
|
||||
//
|
||||
// Declined, not thrown. This used to THROW_EXCEPTION, which unwinds a C++ exception through
|
||||
// the C GL ABI and takes the process down (the hazard GL_Texture.cpp and RenderState.cpp
|
||||
// already name); an application that merely enabled a legal desktop feature died instead of
|
||||
// getting a draw that rendered nothing. VK_NULL_HANDLE is this function's established
|
||||
// "skip this draw" answer, used by the no-stages case above.
|
||||
//
|
||||
// Reached only when this draw's index stream really does restart. Testing the raw
|
||||
// capability bits here instead - which is what it did - refused every NON-INDEXED
|
||||
// list-topology draw as well, so an application that enables GL_PRIMITIVE_RESTART once at
|
||||
// init and then calls glDrawArrays(GL_TRIANGLES, ...) rendered nothing at all.
|
||||
const auto isListTopology = [](VkPrimitiveTopology t) {
|
||||
return t == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST ||
|
||||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST ||
|
||||
t == VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY ||
|
||||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY || t == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
|
||||
t == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
|
||||
};
|
||||
if (primitiveRestartEnabled && !m_primitiveTopologyListRestartFeatureEnabled && isListTopology(vkTopology)) {
|
||||
THROW_EXCEPTION("Primitive restart on a list topology requires the primitiveTopologyListRestart device "
|
||||
"feature (VK_EXT_primitive_topology_list_restart), which this device does not support; use "
|
||||
"a strip/fan topology or a device that supports it.");
|
||||
MGLOG_E_ONCE("Draw skipped: primitive restart on a list topology (0x%x) requires the "
|
||||
"primitiveTopologyListRestart device feature (VK_EXT_primitive_topology_list_restart), "
|
||||
"which this device does not support; use a strip/fan topology, or disable primitive "
|
||||
"restart for list-topology draws.",
|
||||
mode);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
PipelineFactory::PipelineCreatePayload payload {
|
||||
@@ -5050,6 +5192,13 @@ void main() {
|
||||
.renderPass = renderPassEntry.renderPass,
|
||||
.colorAttachmentCount = renderPassEntry.colorAttachmentCount,
|
||||
.rasterizationSamples = renderPassEntry.sampleCount,
|
||||
// ARB_sample_shading. Dropped on a device without sampleRateShading rather than
|
||||
// hard-failing the draw: the rate is a hint, and the pipeline renders correctly at the
|
||||
// driver's own rate. Both halves move the render state's PIPELINE version, so a cached
|
||||
// pipeline built at the old rate cannot be handed back for the new one.
|
||||
.sampleShadingEnable = m_sampleRateShadingFeatureEnabled &&
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading),
|
||||
.minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(),
|
||||
.subpass = 0,
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
@@ -5106,10 +5255,21 @@ void main() {
|
||||
// program with a tessellation stage may only be drawn with GL_PATCHES), so nothing legal
|
||||
// loses its pass-through here; what it does lose is the pipeline, because the refusal
|
||||
// below then sees an evaluation stage with no control stage and declines.
|
||||
//
|
||||
// The default tessellation levels (glPatchParameterfv) are draw state for the same reason
|
||||
// and are compiled into the same module, so they are read here too and their key is mixed
|
||||
// into the pipeline hash - without that a pipeline memoised at one set of levels would be
|
||||
// handed back after the application changed them.
|
||||
if (programObj.needsPassthroughTessControl && programObj.passthroughTessControlEmulatable &&
|
||||
vkTopology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST) {
|
||||
payload.passthroughTessControlStage =
|
||||
m_programFactory->GetOrCreatePassthroughTessControlStage(payload.patchControlPoints);
|
||||
const FloatVec4& defaultOuterLevel = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
const FloatVec2& defaultInnerLevel = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
payload.passthroughTessControlKey = ProgramFactory::ComputePassthroughTessControlKey(
|
||||
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
|
||||
programObj.passthroughPerVertexMembers);
|
||||
payload.passthroughTessControlStage = m_programFactory->GetOrCreatePassthroughTessControlStage(
|
||||
payload.patchControlPoints, defaultOuterLevel, defaultInnerLevel,
|
||||
programObj.passthroughPerVertexMembers);
|
||||
}
|
||||
if (!payload.stencilTestEnable) {
|
||||
payload.frontStencilFailOp = VK_STENCIL_OP_KEEP;
|
||||
@@ -5309,8 +5469,21 @@ void main() {
|
||||
colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format;
|
||||
} else if (colorAttachmentRenderbuffer != nullptr) {
|
||||
textureExternalIndex = static_cast<Int>(colorAttachmentRenderbuffer->GetExternalIndex());
|
||||
colorAttachmentFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(
|
||||
colorAttachmentRenderbuffer->GetInternalFormat());
|
||||
// The SAME resolver GetOrCreateRenderbufferResource backs the image with, so the
|
||||
// probe cannot ask about a format the attachment does not have. The strict 1:1
|
||||
// converter is the wrong question here and answered VK_FORMAT_UNDEFINED for
|
||||
// RGBA2/RGBA12/RGB10/RGB12/RGB16 and the packed 16-bit formats for RGBA4/RGB5_A1
|
||||
// - and VkFormatProperties for UNDEFINED are all zero, so blending was
|
||||
// force-disabled forever on attachments that blend perfectly well. Every
|
||||
// three-channel colour renderbuffer was in that set too (R8G8B8_UNORM is rarely
|
||||
// supported), which is the more ordinary shape.
|
||||
//
|
||||
// Resolved rather than looked up: GetOrCreateRenderbufferResource creates images
|
||||
// and bumps epochs, which a pipeline-state query must not do as a side effect.
|
||||
// A renderbuffer has no device-fallback step after the resolver (unlike the
|
||||
// texture path's D24 -> D32 substitution), so the resolver IS its live format.
|
||||
colorAttachmentFormat =
|
||||
ResolveTextureFormatInfo(colorAttachmentRenderbuffer->GetInternalFormat()).format;
|
||||
} else {
|
||||
auto* texture = colorAttachmentTexture;
|
||||
MOBILEGL_ASSERT(texture != nullptr,
|
||||
@@ -5391,6 +5564,7 @@ void main() {
|
||||
entry.vertexInputHash = vertexLayoutHash;
|
||||
entry.renderPassHash = renderPassHash;
|
||||
entry.pipelineStateHash = pipelineStateHash;
|
||||
entry.primitiveRestartEnable = primitiveRestartEnable;
|
||||
entry.transformFlags = transformFlags;
|
||||
entry.pipeline = pipeline;
|
||||
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
|
||||
@@ -5775,7 +5949,11 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
SetupDrawSnapshot& snap = *snapPtr;
|
||||
if (snap.aspects != aspects.GetRaw() || snap.mode != mode) {
|
||||
// Resolved once for the whole function: it guards the snapshot, keys the pipeline memo
|
||||
// probe below, and is handed to GetOrCreatePipeline on a miss - all three must agree.
|
||||
const Bool drawPrimitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView);
|
||||
if (snap.aspects != aspects.GetRaw() || snap.mode != mode ||
|
||||
snap.primitiveRestartEnable != drawPrimitiveRestartEnable) {
|
||||
return false;
|
||||
}
|
||||
if (m_clearManager->HasAnyPendingClears()) {
|
||||
@@ -6045,6 +6223,7 @@ void main() {
|
||||
entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash &&
|
||||
entry.renderPassHash == snap.renderPassHash &&
|
||||
entry.pipelineStateHash == m_pipelineStateHash &&
|
||||
entry.primitiveRestartEnable == drawPrimitiveRestartEnable &&
|
||||
entry.transformFlags == memoTransformFlags) {
|
||||
pipeline = entry.pipeline;
|
||||
break;
|
||||
@@ -6055,14 +6234,17 @@ void main() {
|
||||
// index, depth/stencil participation, image epochs, no pending clears)
|
||||
// was verified unchanged above, so this is a pure cache hit on the same
|
||||
// entry the snapshot's pipeline was built against.
|
||||
const RenderPassEntry& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
|
||||
const RenderPassEntry* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
|
||||
*drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil);
|
||||
if (!activeRenderPass->CompatibleWith(renderPassEntry)) {
|
||||
// A decline (nullptr) is an attachment DirectVulkan cannot represent; the builder
|
||||
// has already logged it. Fall out of the fast path the same way an incompatible
|
||||
// pass does - the full path re-resolves, declines again and drops the draw.
|
||||
if (renderPassEntry == nullptr || !activeRenderPass->CompatibleWith(*renderPassEntry)) {
|
||||
return false;
|
||||
}
|
||||
pipeline = GetOrCreatePipeline(mode, program, programObj,
|
||||
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
|
||||
vao, renderPassEntry);
|
||||
vao, *renderPassEntry, drawPrimitiveRestartEnable);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
@@ -6447,12 +6629,23 @@ void main() {
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
auto* renderPassEntry =
|
||||
&m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
|
||||
m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
|
||||
// nullptr: the framebuffer has an attachment DirectVulkan cannot represent (a texture the
|
||||
// texture manager declined to back, or a view it could not build). The builder logged which
|
||||
// one; drop the draw here, exactly as an unresolvable sampler descriptor drops one in
|
||||
// BindProgramUniformBuffers. Before this existed the same condition dereferenced a null
|
||||
// resource or handed VK_NULL_HANDLE to vkCreateFramebuffer and took the process down.
|
||||
if (renderPassEntry == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
activeRenderPass = nullptr;
|
||||
renderPassEntry =
|
||||
&m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
|
||||
m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired, drawUsesDepthStencil);
|
||||
if (renderPassEntry == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (renderPassEntry->attachmentCount == 0 || renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
|
||||
MGLOG_D("SetupDraw skipped: drawFbo=%u resolved to an empty render pass (attachmentCount=%u extent=%dx%d)",
|
||||
@@ -6499,7 +6692,8 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry);
|
||||
auto pipeline = GetOrCreatePipeline(mode, program, programObj, transformFlags, vao, *renderPassEntry,
|
||||
ResolvePrimitiveRestartEnable(aspects, pIndexBufferView));
|
||||
// GetOrCreatePipeline documents a VK_NULL_HANDLE return (empty stages, or a driver that
|
||||
// rejected vkCreateGraphicsPipelines). Binding it dereferences null inside the driver -
|
||||
// 9 of the 15 CTS process deaths were exactly this vkCmdBindPipeline. A draw that has no
|
||||
@@ -6562,6 +6756,7 @@ void main() {
|
||||
if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) {
|
||||
snap.valid = true;
|
||||
snap.aspects = aspects.GetRaw();
|
||||
snap.primitiveRestartEnable = ResolvePrimitiveRestartEnable(aspects, pIndexBufferView);
|
||||
snap.mode = mode;
|
||||
snap.programLifetimeId = program.GetLifetimeId();
|
||||
snap.programVersion = program.GetBackendStateVersion();
|
||||
@@ -6796,8 +6991,10 @@ void main() {
|
||||
}
|
||||
|
||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
if (renderPassEntry->attachmentCount == 0 ||
|
||||
auto* renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
// A declined render pass is the same answer as an empty one for a clear: there is nothing
|
||||
// attached that can be cleared inside a pass. The builder has already logged the reason.
|
||||
if (renderPassEntry == nullptr || renderPassEntry->attachmentCount == 0 ||
|
||||
renderPassEntry->extent.x() <= 0 || renderPassEntry->extent.y() <= 0) {
|
||||
return ScissoredClearPrep::NoOp;
|
||||
}
|
||||
@@ -6827,7 +7024,10 @@ void main() {
|
||||
activeRenderPass = nullptr;
|
||||
// Re-resolve: ending the pass updates tracked attachment layouts, which feed the
|
||||
// entry's load ops and initial layouts.
|
||||
renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(framebuffer, m_imageIndexAcquired);
|
||||
if (renderPassEntry == nullptr) {
|
||||
return ScissoredClearPrep::NoOp;
|
||||
}
|
||||
}
|
||||
// A still-active pass is necessarily compatible here: the block above ended any
|
||||
// incompatible one and nothing since can change the active pass.
|
||||
@@ -7992,8 +8192,14 @@ void main() {
|
||||
|
||||
// A color-only blit never touches depth/stencil: let the default-FBO pass
|
||||
// it opens skip the depth attachment (depth-less flavor).
|
||||
auto& renderPassEntry =
|
||||
auto* renderPassEntryPtr =
|
||||
m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired, /*drawUsesDepthStencil=*/false);
|
||||
if (renderPassEntryPtr == nullptr) {
|
||||
// Declined (the builder logged which attachment). The caller's contract for `false` is
|
||||
// "this blit was not serviced here", which is the honest answer.
|
||||
return false;
|
||||
}
|
||||
auto& renderPassEntry = *renderPassEntryPtr;
|
||||
const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry);
|
||||
MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__);
|
||||
|
||||
@@ -8944,6 +9150,12 @@ void main() {
|
||||
VkExtent2D extent = {0, 0};
|
||||
Uint32 depth = 1;
|
||||
Uint32 arrayLayers = 1;
|
||||
// Both resources carry a format; this copy used to decline to read it, which is why a
|
||||
// four-row drift between the texture and renderbuffer format tables turned into
|
||||
// corrupted texels with nothing in the log. vkCmdCopyImage requires size-compatible
|
||||
// formats whenever they differ (VUID-vkCmdCopyImage-srcImage-01548) and there is no
|
||||
// downstream check - a mismatched pair is a promise the driver takes at face value.
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
};
|
||||
|
||||
Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel,
|
||||
@@ -9063,6 +9275,7 @@ void main() {
|
||||
out.extent = resource->extent;
|
||||
out.depth = 1;
|
||||
out.arrayLayers = 1;
|
||||
out.format = resource->format;
|
||||
return out.image != VK_NULL_HANDLE;
|
||||
}
|
||||
// An endpoint that named nothing is the frontend validator's INVALID_VALUE and never
|
||||
@@ -9078,6 +9291,7 @@ void main() {
|
||||
out.extent = resource->extent;
|
||||
out.depth = resource->depth;
|
||||
out.arrayLayers = resource->arrayLayers;
|
||||
out.format = resource->format;
|
||||
return true;
|
||||
};
|
||||
CopyImageVkImage srcImage{};
|
||||
@@ -9118,6 +9332,42 @@ void main() {
|
||||
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
|
||||
return;
|
||||
}
|
||||
// Size compatibility, the guard whose absence let a table drift two files away reach the
|
||||
// driver as a promise. glCopyImageSubData is a raw texel-block move (GL 4.6 core 18.3.2), and
|
||||
// Vulkan says as much: when the two formats differ they must be size-compatible - the same
|
||||
// texel block size - or vkCmdCopyImage is undefined (VUID-vkCmdCopyImage-srcImage-01548).
|
||||
// Nothing else on this path asks: the three checks around it cover the mip range, the region
|
||||
// bounds and the slice range, and none of them ever looked at a format.
|
||||
//
|
||||
// A decline rather than a MOBILEGL_ASSERT, for the reason the neighbouring guards spell out:
|
||||
// assertions compile out of the release build that the CTS and shipping both run, which is
|
||||
// exactly where the corruption was observed.
|
||||
if (srcImage.format != dstImage.format) {
|
||||
// Size-compatibility is the COLOUR rule. Vulkan makes each depth/stencil format compatible
|
||||
// only with ITSELF, and the texel block sizes cannot tell them apart: X8_D24_UNORM_PACK32,
|
||||
// D32_SFLOAT and D24_UNORM_S8_UINT are all 4 bytes and all in different compatibility
|
||||
// classes, so a raw block-size test waves through exactly the pairs Vulkan forbids. The
|
||||
// frontend cannot filter them either - its own texel-block resolver is byte-size only, so
|
||||
// glCopyImageSubData between a GL_DEPTH_COMPONENT24 texture and a GL_DEPTH_COMPONENT32F
|
||||
// one reaches here with two different depth formats and 4 == 4.
|
||||
const Bool eitherIsDepthStencil =
|
||||
((srcImage.aspect | dstImage.aspect) & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
|
||||
if (eitherIsDepthStencil) {
|
||||
MGLOG_E_ONCE("%s: depth/stencil formats are compatible only with themselves, and source format "
|
||||
"%d differs from destination format %d; declining the copy",
|
||||
__func__, static_cast<Int>(srcImage.format), static_cast<Int>(dstImage.format));
|
||||
return;
|
||||
}
|
||||
const Uint32 srcBlockSize = vkuGetFormatInfo(srcImage.format).texel_block_size;
|
||||
const Uint32 dstBlockSize = vkuGetFormatInfo(dstImage.format).texel_block_size;
|
||||
if (srcBlockSize == 0 || dstBlockSize == 0 || srcBlockSize != dstBlockSize) {
|
||||
MGLOG_E_ONCE("%s: source format %d and destination format %d are not size-compatible "
|
||||
"(%u vs %u bytes per texel block); declining the copy",
|
||||
__func__, static_cast<Int>(srcImage.format), static_cast<Int>(dstImage.format),
|
||||
srcBlockSize, dstBlockSize);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const VkImageAspectFlags copyAspectMask =
|
||||
srcImage.aspect & dstImage.aspect &
|
||||
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||
@@ -10190,9 +10440,38 @@ void main() {
|
||||
}
|
||||
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: failed to sync textureId=%u",
|
||||
textureObject->GetExternalIndex());
|
||||
// Two shapes end up in the same place, and for the same reason: the GL level being read has
|
||||
// no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
|
||||
// of its bytes - which makes the shadow both the safe answer and the correct one.
|
||||
//
|
||||
// (a) No VkImage at all. A mutable texture whose GL level 0 was never defined -
|
||||
// glTexImage2D(GL_TEXTURE_2D, 5, ...) and nothing else, exactly what the
|
||||
// clear_tex_image conformance cases build. VkTextureManager takes storage mip 0 as the
|
||||
// physical image extent (CheckMipmapCompleteness), so it refuses to back the texture.
|
||||
// (b) A VkImage with FEWER mip levels than the GL level count. GetUploadMipLevelCount
|
||||
// breaks at the first level with a zero extent, so "level 0 defined, a gap, level 3
|
||||
// defined" produces a one-mip image while GL_TEXTURE_MAX_LEVEL-style state still
|
||||
// reports four levels. The same clamp also fires on a base level small enough that the
|
||||
// full chain is shorter than the levels the application defined.
|
||||
//
|
||||
// (b) is the dangerous one and is why the level is bounded against the RESOURCE and not only
|
||||
// against the GL-side count above: writing that level into imageSubresource.mipLevel is an
|
||||
// out-of-range subresource, which is the promise the driver takes at face value. The
|
||||
// glCopyImageSubData path two functions up carries the same guard for the same reason, added
|
||||
// after it SIGSEGV'd inside the Adreno driver; the readback never had one.
|
||||
const Bool hasImage = resource != nullptr && resource->image != VK_NULL_HANDLE;
|
||||
const Bool levelIsBacked =
|
||||
hasImage && ToStorageMipLevel(textureObject.get(), level) < resource->mipLevels;
|
||||
if (!levelIsBacked) {
|
||||
// Never gated on "syncing was inconvenient": a blanket shadow answer would silently
|
||||
// return stale bytes for every render-to-texture result.
|
||||
MGLOG_D("DirectVulkan::GetTexImage: textureId=%u level %d has no GPU storage (%s); answering "
|
||||
"from the CPU shadow",
|
||||
textureObject->GetExternalIndex(), level,
|
||||
hasImage ? "the image has fewer mip levels" : "the texture has no VkImage");
|
||||
MG_Impl::GLImpl::CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format,
|
||||
type, bufSize, pixels,
|
||||
"DirectVulkan::GetTextureImage");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10351,15 +10630,23 @@ void main() {
|
||||
|
||||
void VulkanRenderer::GenerateMipmap(GLenum target) {
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
// The other mipmappable targets - 1D, 1D array, cube map array - are legal GL and the front
|
||||
// end lets them through, so reaching one here is a coverage gap in this backend, not a
|
||||
// broken invariant. Declining leaves the mip chain unwritten; asserting took the process
|
||||
// down with it.
|
||||
// Whatever is left here is a coverage gap in this backend, not a broken invariant, so it
|
||||
// declines (leaving the mip chain unwritten) rather than asserting the process down. What
|
||||
// remains is the multisample targets, which GL 4.6 core 8.14.4 forbids to glGenerateMipmap
|
||||
// outright.
|
||||
//
|
||||
// Every ARRAY target - 1D array, 2D array, cube map array - needs no blit code of its own:
|
||||
// its layers live in the VkImage's arrayLayers, so resource->extent/depth already describe
|
||||
// one layer's image and the loop below already copies every layer per level via
|
||||
// srcSubresource.layerCount = resource->arrayLayers. The one thing they DO need is that the
|
||||
// GL-space storage allocation not shrink the layer count down the chain, which
|
||||
// MipShrinkingComponentCount handles.
|
||||
if (textureTarget != TextureTarget::Texture2D && textureTarget != TextureTarget::Texture2DArray &&
|
||||
textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::TextureCubeMap &&
|
||||
textureTarget != TextureTarget::TextureCubeMapArray &&
|
||||
// A 1D texture needs nothing special: its storage extent is {width, 1, 1}, so the blit
|
||||
// loop below already emits the y and z offsets of 0 and 1 that a 1D image requires.
|
||||
textureTarget != TextureTarget::Texture1D) {
|
||||
textureTarget != TextureTarget::Texture1D && textureTarget != TextureTarget::Texture1DArray) {
|
||||
MGLOG_W_ONCE("GenerateMipmap: unsupported target %s", MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
|
||||
return;
|
||||
}
|
||||
@@ -12709,6 +12996,11 @@ void main() {
|
||||
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
||||
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
||||
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
|
||||
// ARB_sample_shading. Without this feature a pipeline may not set sampleShadingEnable
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784), so the GL enable
|
||||
// has to be dropped rather than forwarded - which is what the flag below records.
|
||||
deviceFeatures.sampleRateShading = supportedDeviceFeatures.sampleRateShading;
|
||||
m_sampleRateShadingFeatureEnabled = deviceFeatures.sampleRateShading == VK_TRUE;
|
||||
// ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly
|
||||
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
|
||||
// gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are
|
||||
@@ -12976,6 +13268,55 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// VK_EXT_custom_border_color: an arbitrary GL_TEXTURE_BORDER_COLOR, in float or integer form,
|
||||
// instead of the four predefined VkBorderColor values. Without it a border outside
|
||||
// transparent black / opaque black / opaque white has to be snapped, which is what made every
|
||||
// border texel of a GL_RGBA8 texture with border (255,255,255,255) sample as 0 and what made
|
||||
// an integer border of -1 come back as 0.
|
||||
//
|
||||
// customBorderColorWithoutFormat is required alongside customBorderColors, not merely
|
||||
// preferred: a GL sampler object carries a border colour with no idea which texture it will
|
||||
// be paired with, so the VkSamplerCustomBorderColorCreateInfoEXT this backend builds has to
|
||||
// leave `format` VK_FORMAT_UNDEFINED.
|
||||
m_customBorderColorFeatureEnabled = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
VkPhysicalDeviceCustomBorderColorFeaturesEXT customBorderColorFeatures{};
|
||||
customBorderColorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT;
|
||||
if (IsExtensionSupported(availableExtensions, VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME) &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &customBorderColorFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
if (customBorderColorFeatures.customBorderColors == VK_TRUE &&
|
||||
customBorderColorFeatures.customBorderColorWithoutFormat == VK_TRUE) {
|
||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
||||
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
|
||||
}
|
||||
customBorderColorFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &customBorderColorFeatures;
|
||||
m_customBorderColorFeatureEnabled = true;
|
||||
|
||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
||||
VkPhysicalDeviceCustomBorderColorPropertiesEXT customBorderColorProperties{};
|
||||
customBorderColorProperties.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_PROPERTIES_EXT;
|
||||
VkPhysicalDeviceProperties2 propertyQuery{};
|
||||
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||
propertyQuery.pNext = &customBorderColorProperties;
|
||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
|
||||
m_maxCustomBorderColorSamplers = customBorderColorProperties.maxCustomBorderColorSamplers;
|
||||
}
|
||||
MGLOG_I("Enabled optional device extension: %s (maxCustomBorderColorSamplers=%u)",
|
||||
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, m_maxCustomBorderColorSamplers);
|
||||
}
|
||||
}
|
||||
if (!m_customBorderColorFeatureEnabled) {
|
||||
MGLOG_I("%s unavailable; GL_TEXTURE_BORDER_COLOR snaps to the nearest predefined VkBorderColor",
|
||||
VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
// Native subgroup topology, and VK_EXT_subgroup_size_control's
|
||||
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
|
||||
// turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from
|
||||
|
||||
@@ -584,6 +584,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
|
||||
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
|
||||
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
|
||||
// colour outside the palette has to be snapped to the nearest predefined one. Both features
|
||||
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
|
||||
// colour without naming the image format it will be paired with, which GL's sampler objects
|
||||
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
|
||||
// to be able to fall back to the snapped value once it is reached.
|
||||
Bool m_customBorderColorFeatureEnabled = false;
|
||||
Uint32 m_maxCustomBorderColorSamplers = 0;
|
||||
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
|
||||
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
|
||||
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
|
||||
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
|
||||
Bool m_sampleRateShadingFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
@@ -733,6 +749,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
|
||||
// from anything else in this key: it depends on whether the draw is indexed and on the
|
||||
// index type, neither of which the mode/program/state hashes carry. Without it an
|
||||
// indexed and a non-indexed draw over the same program and state collide on one entry
|
||||
// and the second one gets the first one's restart setting.
|
||||
Bool primitiveRestartEnable = false;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
static constexpr Uint32 kPipelineMemoSize = 8;
|
||||
@@ -865,6 +887,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 bindGeneration = 0;
|
||||
Uint32 baseTransformFlags = 0;
|
||||
Uint32 resolvedTransformFlags = 0;
|
||||
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
|
||||
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
|
||||
// already separates indexed from non-indexed draws, but not one index TYPE from
|
||||
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
|
||||
// makes those two draws want different pipelines.
|
||||
Bool primitiveRestartEnable = false;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint32 imageIndex = 0;
|
||||
Uint64 textureEraseEpoch = 0;
|
||||
@@ -1168,13 +1196,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void CreateSwapchain();
|
||||
void CreateCommandPool();
|
||||
|
||||
// Whether THIS draw's primitive stream restarts, and therefore what
|
||||
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
|
||||
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
|
||||
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
|
||||
// and the index TYPE (an application restart index that does not fit the type matches no
|
||||
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
|
||||
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
|
||||
const IndexBufferView* pIndexBufferView) const;
|
||||
|
||||
VkPipeline GetOrCreatePipeline(
|
||||
GLenum mode,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
ProgramFactory::CompileOptionFlags transformFlags,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
const RenderPassEntry& renderPassEntry,
|
||||
Bool primitiveRestartEnable);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
|
||||
@@ -34,8 +34,81 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
|
||||
// Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
|
||||
// is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
|
||||
// backend samples during this draw describes the program it is drawing), so the draw funnel
|
||||
// below resolves it exactly once and hands it to both users.
|
||||
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const char* functionName) {
|
||||
// "If there is no current program object or bound program pipeline object, the results of
|
||||
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
|
||||
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
|
||||
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
|
||||
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
|
||||
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
|
||||
// the compute shader stage"), which is why this lives on the draw path and not in the
|
||||
// shared ValidateProgramForExecution below.
|
||||
if (!currentProgram) return false;
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
|
||||
// inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
|
||||
// evaluation or geometry stage has an executable but no program supplies an executable
|
||||
// VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
|
||||
// shape - so in practice it catches a program pipeline assembled out of stage programs,
|
||||
// which today draws happily and renders nothing.
|
||||
//
|
||||
// Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
|
||||
// program is the graphics composite, whose linked-shader snapshot is built out of exactly
|
||||
// the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
|
||||
// stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
|
||||
// deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
|
||||
// nothing else here should start refusing draws GL accepts.
|
||||
//
|
||||
// On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
|
||||
// which shares that helper and legitimately has no vertex stage - is untouched.
|
||||
const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
|
||||
currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
||||
if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
|
||||
// default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
|
||||
// for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
|
||||
// drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
|
||||
// rules out baking it at link time.
|
||||
//
|
||||
// Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
|
||||
// decides the value and either half can move between draws. It costs a phase-A flag read for
|
||||
// every program that has no shim, and a 4-byte compare for the ones that do: the write only
|
||||
// bumps the UBO content version when the number actually changes, so a run of draws into one
|
||||
// framebuffer re-uploads nothing.
|
||||
static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
||||
if (!program || !program->UsesReservedNumSamples()) return;
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
|
||||
// when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
|
||||
}
|
||||
|
||||
// The one funnel every drawing command passes through. Order is load-bearing: validate first
|
||||
// (a rejected draw must leave state alone), then publish the sample count - which reads the
|
||||
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
|
||||
// and before the backend consumes the program's UBO content version.
|
||||
static Bool PrepareCurrentProgramForDraw(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
|
||||
PublishDrawFramebufferSampleCount(currentProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
|
||||
@@ -713,6 +786,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
|
||||
// NO control stage tessellates at; both backends have to synthesize that control stage
|
||||
// themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
|
||||
// there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
|
||||
// pname is the only error the spec lists: any float values are accepted, negatives and NaN
|
||||
// included, and it is the tessellator that clamps them.
|
||||
//
|
||||
// This used to be a stub, which is why the two synthesizers hardcoded 1.0.
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values) {
|
||||
if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
|
||||
return;
|
||||
}
|
||||
if (!values) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
|
||||
return;
|
||||
}
|
||||
if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
|
||||
MG_State::pGLContext->SetPatchDefaultOuterLevel(
|
||||
FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else {
|
||||
MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
|
||||
// barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
|
||||
@@ -751,6 +855,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
memoryBarrier(barriers);
|
||||
}
|
||||
|
||||
void TextureBarrier() {
|
||||
// GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
|
||||
// framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
|
||||
// read texels of a texture that is also attached to the current framebuffer.
|
||||
//
|
||||
// Both backends serve this through their existing memory-barrier hook rather than a new
|
||||
// entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
|
||||
// writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
|
||||
// is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
|
||||
// scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
|
||||
// subset, so it cannot under-synchronize.
|
||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||
if (!memoryBarrier) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
|
||||
return;
|
||||
}
|
||||
memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
}
|
||||
|
||||
void MemoryBarrierByRegion(GLbitfield barriers) {
|
||||
if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
|
||||
auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
|
||||
@@ -766,14 +891,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||
}
|
||||
@@ -851,7 +976,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
|
||||
// they produce for a call made with no program bound. Same precedence decision, and
|
||||
// the same reason, as DispatchComputeIndirect above.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
|
||||
if (!multiDrawElementsIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -872,7 +997,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
// See MultiDrawElementsIndirectCount, including why this one goes last.
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
|
||||
if (!multiDrawArraysIndirectCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -887,7 +1012,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
@@ -897,7 +1022,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
||||
}
|
||||
@@ -905,7 +1030,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||
baseinstance);
|
||||
@@ -914,7 +1039,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
@@ -925,21 +1050,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||
GLsizei instancecount, GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
||||
}
|
||||
|
||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
|
||||
@@ -949,21 +1074,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||
GLuint baseinstance) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
||||
}
|
||||
|
||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
||||
}
|
||||
|
||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||
DrawArraysIndirect_Backend(mode, indirect);
|
||||
@@ -971,7 +1096,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
|
||||
@@ -981,7 +1106,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawArrays_Backend(mode, first, count);
|
||||
@@ -989,7 +1114,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (drawcount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1003,7 +1128,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||
}
|
||||
@@ -1011,7 +1136,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||
if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
|
||||
@@ -1035,7 +1160,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||
if (!PrepareCurrentProgramForDraw(__func__)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||
AccountTransformFeedbackPrimitives(mode, count);
|
||||
DrawElements_Backend(mode, count, type, indices);
|
||||
@@ -1459,7 +1584,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// (GL 4.6 core 10.3.7).
|
||||
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
|
||||
GLsizei instancecount) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return;
|
||||
if (!PrepareCurrentProgramForDraw(functionName)) return;
|
||||
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
|
||||
if (instancecount < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1482,8 +1607,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::to_string(id) + " is not a transform feedback object name."));
|
||||
return;
|
||||
}
|
||||
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
|
||||
if (stream != 0) {
|
||||
// GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
|
||||
// answers as 1 - so stream 0 is the only one that exists and anything else is
|
||||
// INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
|
||||
// never drift: if vertex-stream support ever lands, this bound moves with the limit.
|
||||
GLint maxVertexStreams = 1;
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
@@ -1501,6 +1631,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
|
||||
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
|
||||
if (vertices == 0) return;
|
||||
const auto count = static_cast<GLsizei>(vertices);
|
||||
|
||||
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||
void DispatchComputeIndirect(GLintptr indirect);
|
||||
void PatchParameteri(GLenum pname, GLint value);
|
||||
void PatchParameterfv(GLenum pname, const GLfloat* values);
|
||||
void MemoryBarrier(GLbitfield barriers);
|
||||
void MemoryBarrierByRegion(GLbitfield barriers);
|
||||
void TextureBarrier();
|
||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
|
||||
@@ -160,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
|
||||
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
|
||||
@@ -411,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
||||
@@ -923,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
||||
@@ -994,7 +994,7 @@ DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||
@@ -1107,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
|
||||
@@ -1150,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
|
||||
@@ -2049,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
|
||||
@@ -2546,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, )
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
|
||||
|
||||
@@ -474,6 +474,75 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
|
||||
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
|
||||
// one another in that section and a CTS case that walks the family must not get five
|
||||
// different answers - which is exactly what happened when these lived in one helper that
|
||||
// only two of the five went through.
|
||||
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::FramebufferObject>&
|
||||
framebufferObject,
|
||||
FramebufferAttachmentType attachmentType) {
|
||||
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
|
||||
// than or equal to MAX_COLOR_ATTACHMENTS."
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
|
||||
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
|
||||
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
|
||||
// object is always there, and framebuffer 0 has to be recognised by identity instead,
|
||||
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
|
||||
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
|
||||
// from what the swapchain keeps publishing.
|
||||
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
|
||||
if (!framebufferObject ||
|
||||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"No framebuffer object is bound to the target; the default framebuffer's attachments "
|
||||
"cannot be named."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
|
||||
// texture with immutable storage it "must be smaller than the number of levels the texture
|
||||
// has". Split from the attachment-point half because the caller only has a texture object
|
||||
// once the detach (texture == 0) case is behind it.
|
||||
Bool ValidateFramebufferTextureLevel(const char* functionName,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLint level) {
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Texture level must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (!textureObject || !textureObject->IsImmutable()) {
|
||||
// A mutable texture has no level bound here: a level it has not specified yet is
|
||||
// not an error, it just leaves the framebuffer incomplete.
|
||||
return true;
|
||||
}
|
||||
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
|
||||
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
|
||||
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
|
||||
// two-level view onto a ten-level texture would accept level 5 and attach an image
|
||||
// nothing can draw into.
|
||||
const Uint levelBound = textureObject->GetAddressableLevelCount();
|
||||
if (static_cast<Uint>(level) >= levelBound) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
|
||||
levelBound)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
|
||||
GLuint texture, GLint level,
|
||||
TextureUploadTarget textureUploadTarget, Bool layered = false) {
|
||||
@@ -482,10 +551,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
|
||||
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
|
||||
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
|
||||
// which half is being recorded - and dropping it here (the parameter defaults to
|
||||
// false) recorded a non-layered depth/stencil attachment beside a layered colour
|
||||
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
|
||||
// own rule, and downstream it means the depth/stencil attachment covers layer 0
|
||||
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
|
||||
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
|
||||
// attached one layer of it beside a layered colour target, which the driver
|
||||
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
|
||||
// produced nothing. This is the shape
|
||||
// texture_cube_map_array.stencil_attachments_*_layered and
|
||||
// geometry_shader.layered_framebuffer.stencil_support are built on.
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
|
||||
textureUploadTarget);
|
||||
textureUploadTarget, layered);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -497,13 +580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -518,6 +595,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -624,16 +702,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
|
||||
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||
// The multisample TEXTURE path resolves the limit per format the same way
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
|
||||
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
|
||||
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
|
||||
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
|
||||
// count it actually hands the driver.
|
||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
|
||||
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
|
||||
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
|
||||
// integer path is genuinely one sample, and accepting four only moved the failure from an
|
||||
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
|
||||
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
|
||||
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
|
||||
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
|
||||
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
const SizeT formatIndex = static_cast<SizeT>(format);
|
||||
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
|
||||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
|
||||
return 0;
|
||||
}
|
||||
const auto& sampleCounts =
|
||||
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
||||
return sampleCounts.empty() ? 0 : sampleCounts.front();
|
||||
}
|
||||
|
||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
@@ -644,13 +739,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
&normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
|
||||
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
|
||||
// supported for internalformat (see GetInternalformativ)", and
|
||||
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
|
||||
// list. It was never consulted here - the TODO that deferred it was written before the
|
||||
// query was backed and had gone stale - so a format whose multisample probes fail inside
|
||||
// a category that allows four was accepted at four, quietly allocated at one by
|
||||
// ClampSamplesToBackendSupport, and then reported as four by
|
||||
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
|
||||
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
|
||||
if (probedMaxSamples > 0) {
|
||||
return probedMaxSamples;
|
||||
}
|
||||
if (!isIntegerFormat) {
|
||||
return GetMaxRenderbufferSamples_State();
|
||||
}
|
||||
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
|
||||
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
|
||||
// clamps to it, while the frontend honours what it advertised.
|
||||
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
|
||||
return GetAdvertisedIntegerMaxSamples();
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||
@@ -682,8 +788,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Resolve the remaining per-internalformat renderbuffer sample limits once
|
||||
// glGetInternalformativ is backed; integer formats are handled below.
|
||||
// Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
|
||||
// to the format's category pname where nothing was probed. (This carried a TODO deferring
|
||||
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
|
||||
// both renderbuffers and multisample textures since, so the deferral was collected.)
|
||||
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
|
||||
if (samples > maxSamples) {
|
||||
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports
|
||||
@@ -1048,13 +1156,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
auto& framebufferObject = bindingSlot.GetBoundObject();
|
||||
if (!framebufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1069,6 +1171,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
|
||||
if (layer < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -1191,6 +1294,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Framebuffer target is bound to no framebuffer object."));
|
||||
return;
|
||||
}
|
||||
// glFramebufferTexture2D is by far the most-used member of the family and the only one
|
||||
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
|
||||
// conditions have to be asked here explicitly.
|
||||
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
|
||||
attachmentType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
framebufferObject->Detach(attachmentType);
|
||||
@@ -1205,6 +1315,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
|
||||
|
||||
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
|
||||
if (expectedTextureTarget == TextureTarget::Unknown ||
|
||||
@@ -1241,6 +1352,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
|
||||
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
|
||||
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
|
||||
// every texture name that was never generated.
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
if (!textureObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -1291,13 +1408,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Texture object {} is not valid.", texture)));
|
||||
return;
|
||||
}
|
||||
if (level < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State",
|
||||
"Texture level must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// The whole level condition, not just its negative half: glNamedFramebufferTexture and
|
||||
// glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
|
||||
// be rejected on both or a CTS case gets two answers for one rule.
|
||||
if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
|
||||
|
||||
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
|
||||
Bool layered = false;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "GL_Getter.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
|
||||
@@ -93,8 +95,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// limits they advertise still have to be legal.
|
||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||
// The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second
|
||||
// literal: they used to be independent (4096 components against 128 vectors, 64 varying
|
||||
// components against 8 varying vectors) and could not both be describing the same
|
||||
// capacity. Both are shared with BuildTBuiltInResource through Types.h, because
|
||||
// gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers.
|
||||
constexpr GLint kFrontendMaxVertexUniformComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVertexUniformVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS);
|
||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxVertexOutputComponents = 64;
|
||||
constexpr GLint kFrontendMaxFragmentInputComponents = 128;
|
||||
@@ -106,21 +115,61 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16;
|
||||
constexpr GLint kFrontendMaxGeometryUniformComponents = 1024;
|
||||
constexpr GLint kFrontendMaxGeometryUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks +
|
||||
kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks;
|
||||
constexpr GLint kFrontendMaxVaryingComponents = 64;
|
||||
constexpr GLint kFrontendMaxVaryingVectors = 8;
|
||||
// ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no
|
||||
// gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter
|
||||
// answer only; 32 is the GL 4.6 core minimum (table 23.57).
|
||||
constexpr GLint kFrontendMaxGeometryShaderInvocations = 32;
|
||||
constexpr GLint kFrontendMaxTessControlUniformBlocks = 14;
|
||||
constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14;
|
||||
// The compute stage's share of the combined sum below. Compute's own per-stage answer is
|
||||
// backend-derived (GL_MAX_COMPUTE_UNIFORM_BLOCKS reads dynamicParameters), so this is not
|
||||
// what that query returns - it is the GL 4.3 core minimum, present here only so the
|
||||
// combined total covers all SIX stages.
|
||||
constexpr GLint kFrontendMaxComputeUniformBlocksShare = 14;
|
||||
// GL 4.6 table 23.64 orders MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >=
|
||||
// every per-stage count, and the sum has to run over SIX stages, not three and not five.
|
||||
// Three (42) was the original bug. Five (70) replaced it and broke the middle term the
|
||||
// other way: compute's per-stage count is backend-derived and clamps at the binding count,
|
||||
// so a device reporting descriptor-indexing-scale uniform buffers (Adreno reports
|
||||
// maxPerStageDescriptorUniformBuffers = 16777216) advertised 84 compute blocks against a
|
||||
// combined 70. Six stages x 14 = 84, which is also exactly the binding-point count and the
|
||||
// arithmetic the GL 4.5 minimum of 84 bindings is built from, so the ordering is now tight
|
||||
// rather than accidental.
|
||||
constexpr GLint kFrontendMaxCombinedUniformBlocks =
|
||||
kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks +
|
||||
kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks +
|
||||
kFrontendMaxFragmentUniformBlocks + kFrontendMaxComputeUniformBlocksShare;
|
||||
constexpr GLint kFrontendMaxVaryingComponents =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS);
|
||||
constexpr GLint kFrontendMaxVaryingVectors =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS);
|
||||
constexpr GLint kFrontendMaxProgramTexelOffset = 7;
|
||||
constexpr GLint kFrontendMinProgramTexelOffset = -8;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4;
|
||||
constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4;
|
||||
// ARB_transform_feedback3's vertex-stream count. One is what this implementation can
|
||||
// actually emit to; see the GL_MAX_VERTEX_STREAMS case for why it is not four.
|
||||
constexpr GLint kFrontendMaxVertexStreams = 1;
|
||||
constexpr GLint kFrontendMaxGeometryOutputVertices = 256;
|
||||
constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024;
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 36;
|
||||
// GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly
|
||||
// how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the
|
||||
// GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not
|
||||
// the ceiling here.
|
||||
constexpr GLint kFrontendMinUniformBufferBindings = 84;
|
||||
constexpr GLint kFrontendSubpixelBits = 4;
|
||||
constexpr GLint kFrontendMaxSamples = 4;
|
||||
constexpr GLint kFrontendMaxSamples =
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES);
|
||||
// ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no
|
||||
// glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum
|
||||
// plumbing. These are answered - with the GL 4.5 core minimums - because the conformance
|
||||
// suite queries them before it checks for the feature and an INVALID_ENUM both leaves the
|
||||
// caller reading its own uninitialised stack slot and strands an error for the next
|
||||
// unrelated call to trip over. The extension is deliberately NOT advertised, so the
|
||||
// numbers are a table entry, not a capability claim.
|
||||
constexpr GLint kFrontendMaxSubroutines = 256;
|
||||
constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024;
|
||||
|
||||
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
||||
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
||||
@@ -134,9 +183,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.64: components + blocks * (blockSize / 4). The product has to be
|
||||
// formed in 64 bits and saturated on the way out - it overflowed a signed 32-bit int on
|
||||
// every Vulkan host that reports a large maxUniformBufferRange. A Mali driver answering
|
||||
// 0xFFFFFFFF saturates to INT32_MAX in the loader, and 14 * (2147483647 / 4) + 4096 wraps
|
||||
// to -1073737742, which the conformance suite read back as a limit "smaller than 58368".
|
||||
// Saturating instead of wrapping is also the only honest answer: an implementation that
|
||||
// can serve more components than a GLint holds still has to report a GLint.
|
||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||
GLint maxUniformBlockSizeBytes) {
|
||||
return maxDefaultUniformComponents + maxUniformBlocks * (maxUniformBlockSizeBytes / 4);
|
||||
const Int64 blocks = std::max<Int64>(static_cast<Int64>(maxUniformBlocks), 0);
|
||||
const Int64 componentsPerBlock = std::max<Int64>(static_cast<Int64>(maxUniformBlockSizeBytes), 0) / 4;
|
||||
const Int64 total = static_cast<Int64>(maxDefaultUniformComponents) + blocks * componentsPerBlock;
|
||||
return static_cast<GLint>(std::min<Int64>(total, std::numeric_limits<GLint>::max()));
|
||||
}
|
||||
|
||||
bool TryDecodeIndexedBufferQuery(GLenum pname, BufferTarget& bufferTarget, IndexedBufferQueryKind& queryKind) {
|
||||
@@ -304,24 +363,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
void RecordIndexedOnlyGetterError(const char* functionName, GLenum pname) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -473,10 +514,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
||||
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
||||
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
||||
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
||||
// just told the application to use. The backends clamp the realised count instead.
|
||||
// before it is advertised. gl_MaxSamples expands from the same floored number
|
||||
// (BuildTBuiltInResource), which is also what sizes gl_SampleMask[].
|
||||
//
|
||||
// THE FLOOR STOPS HERE, and that is the point. It used to be applied to
|
||||
// GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too,
|
||||
// on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to
|
||||
// every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the
|
||||
// reasoning had it backwards: Adreno and Mali back an integer multisample texture with a
|
||||
// single sample, so flooring the query at 4 did not make four samples exist - it made the
|
||||
// backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote
|
||||
// per-sample data it could never read back. Reporting what was probed turns that into an
|
||||
// honest "unsupported" the application can branch on.
|
||||
GLint GetAdvertisedMaxSamples() {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return kFrontendMaxSamples;
|
||||
@@ -484,6 +533,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four:
|
||||
// see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is
|
||||
// floored rather than trusted.
|
||||
namespace {
|
||||
GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GLint GetAdvertisedColorTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedDepthTextureMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples);
|
||||
}
|
||||
|
||||
GLint GetAdvertisedIntegerMaxSamples() {
|
||||
return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples);
|
||||
}
|
||||
|
||||
// Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved
|
||||
// gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports.
|
||||
GLint ResolveDrawFramebufferSampleCount() {
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (!drawFbo) return 0;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
|
||||
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
|
||||
} else if (attachment.IsTexture() && attachment.GetTexture()) {
|
||||
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
|
||||
// report 1 for any multisampled draw framebuffer).
|
||||
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
|
||||
}
|
||||
}
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
const GLubyte* GetString(GLenum name) {
|
||||
static String vendorString;
|
||||
@@ -680,12 +773,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||
// Same reason as the three above: the integer fallback would round the fraction to 0
|
||||
// or 1 first, so a 0.25 sample-shading rate would answer GL_FALSE.
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE: {
|
||||
GLfloat value = 0.0f;
|
||||
GetFloatv(pname, &value);
|
||||
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
}
|
||||
// Float-native state, so GL 4.6 core 2.2.2's "zero becomes FALSE, every other value
|
||||
// becomes TRUE" has to be applied to the VALUE. Answering these through the integer getter
|
||||
// below instead - which rounds - reported GL_FALSE for a perfectly non-zero level of 0.25,
|
||||
// and every other float state in this function already reads through GetFloatv for exactly
|
||||
// that reason.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const GLsizei componentCount = pname == GL_PATCH_DEFAULT_OUTER_LEVEL ? 4 : 2;
|
||||
GLfloat levels[4] = {};
|
||||
GetFloatv(pname, levels);
|
||||
for (GLsizei i = 0; i < componentCount; ++i) {
|
||||
params[i] = levels[i] != 0.0f ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -735,6 +846,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = depthRange.y();
|
||||
return;
|
||||
}
|
||||
// glPatchParameterfv's two states. Float-native, so they are answered here rather than
|
||||
// through the integer fallback below - which rounds, and would report 0 for a level of 0.5.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
params[0] = outer.x();
|
||||
params[1] = outer.y();
|
||||
params[2] = outer.z();
|
||||
params[3] = outer.w();
|
||||
return;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
params[0] = inner.x();
|
||||
params[1] = inner.y();
|
||||
return;
|
||||
}
|
||||
case GL_VIEWPORT_BOUNDS_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.ViewportBoundsRangeMin;
|
||||
@@ -800,6 +927,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POLYGON_OFFSET_UNITS:
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetUnits();
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float-native state, so it is answered here rather than through the integer
|
||||
// fallback: glPolygonOffsetClamp(1, 1, 0.5) must read back as 0.5, not as 0.
|
||||
params[0] = MG_State::pGLContext->GetPolygonOffsetClamp();
|
||||
return;
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE: {
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
params[0] = dynamicParameters.SmoothLineWidthRangeMin;
|
||||
@@ -815,6 +947,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_COVERAGE_VALUE:
|
||||
params[0] = MG_State::pGLContext->GetSampleCoverageValue();
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// Float state, so it has to be answered here rather than through the integer
|
||||
// fallback: glMinSampleShading(0.5) must read back as 0.5 and not as 0.
|
||||
params[0] = MG_State::pGLContext->GetMinSampleShadingValue();
|
||||
return;
|
||||
case GL_POINT_FADE_THRESHOLD_SIZE:
|
||||
// Float state: read it directly so the fractional part is not lost to the integer path.
|
||||
params[0] = MG_State::pGLContext->GetPointFadeThresholdSize();
|
||||
@@ -1186,6 +1323,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
switch (pname) {
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
// The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and
|
||||
// not left to the 32-bit fallback below: the conformance suite reads it with
|
||||
// glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the
|
||||
// spec requires 2^32-1.
|
||||
params[0] = 0xFFFFFFFFLL;
|
||||
return;
|
||||
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
params[0] = static_cast<GLint64>(
|
||||
@@ -1222,12 +1366,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLint ints[4] = {};
|
||||
GetIntegerv(pname, ints);
|
||||
|
||||
// GL 4.6 core 22.1 gives glGetInteger64v the same accepted-pname set as glGetIntegerv, so
|
||||
// every pname the integer getter answers with several components owes them all here too.
|
||||
// A pname that reaches the `default:` arm writes params[0] and leaves the caller's other
|
||||
// components holding whatever they held, with no error to say so.
|
||||
switch (pname) {
|
||||
case GL_BLEND_COLOR:
|
||||
case GL_COLOR_CLEAR_VALUE:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_VIEWPORT:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
params[i] = static_cast<GLint64>(ints[i]);
|
||||
}
|
||||
@@ -1237,6 +1386,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
params[0] = static_cast<GLint64>(ints[0]);
|
||||
params[1] = static_cast<GLint64>(ints[1]);
|
||||
return;
|
||||
@@ -1268,6 +1418,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_POINT_SIZE_RANGE:
|
||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||
case GL_MAX_VIEWPORT_DIMS:
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL:
|
||||
count = 2;
|
||||
break;
|
||||
case GL_BLEND_COLOR:
|
||||
@@ -1275,6 +1426,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_COLOR_WRITEMASK:
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL:
|
||||
count = 4;
|
||||
break;
|
||||
default:
|
||||
@@ -1314,6 +1466,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
// GL_TEXTURE_BUFFER_BINDING and GL_TEXTURE_BUFFER are the same token (0x8C2A): as a
|
||||
// glGetIntegerv pname it asks which BUFFER object is bound to the buffer-texture target,
|
||||
// not which texture is (that one is GL_TEXTURE_BINDING_BUFFER, handled by the texture-unit
|
||||
// decoder above).
|
||||
case GL_TEXTURE_BUFFER_BINDING: {
|
||||
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Texture).GetBoundObject();
|
||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
case GL_BLEND:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Blend) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
@@ -1369,6 +1530,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// this single case serves every getter flavor.
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClampReadColor());
|
||||
return;
|
||||
// glClipControl's two state variables (GL 4.5 core table 23.7). They answer from the
|
||||
// state the entry point records, which is what the conformance suite's initial-value and
|
||||
// set-then-get cases read - the RASTERIZATION half of clip control is a separate,
|
||||
// backend-side question and does not gate the query.
|
||||
case GL_CLIP_ORIGIN:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipOrigin());
|
||||
return;
|
||||
case GL_CLIP_DEPTH_MODE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetClipDepthMode());
|
||||
return;
|
||||
case GL_COLOR_CLEAR_VALUE: {
|
||||
const FloatVec4& clearColor = MG_State::pGLContext->GetClearColor();
|
||||
params[0] = static_cast<GLint>(clearColor.x());
|
||||
@@ -1657,6 +1828,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryUniformComponents;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_SHADER_INVOCATIONS:
|
||||
*params = kFrontendMaxGeometryShaderInvocations;
|
||||
return;
|
||||
case GL_MAX_IMAGE_SAMPLES:
|
||||
*params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend
|
||||
return;
|
||||
@@ -1710,6 +1884,59 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params =
|
||||
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
||||
return;
|
||||
// The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in
|
||||
// constant that BuildTBuiltInResource expands, and the two must report the same number
|
||||
// (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from
|
||||
// the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here.
|
||||
// They were the whole per-stage tess family: the table had been filled in only where the
|
||||
// honest answer was zero (the atomic counters, the image uniforms) or where a driver
|
||||
// query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose
|
||||
// answer is a real resource count fell through to GL_INVALID_ENUM.
|
||||
case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS);
|
||||
return;
|
||||
case GL_MAX_TESS_PATCH_COMPONENTS:
|
||||
*params = static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS);
|
||||
return;
|
||||
// Routed through the same clamp as every other per-stage block count so the
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of
|
||||
// GL 4.6 table 23.64 cannot be broken by the two families moving independently.
|
||||
case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS:
|
||||
*params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks);
|
||||
return;
|
||||
case GL_MAX_SUBROUTINES:
|
||||
*params = kFrontendMaxSubroutines;
|
||||
return;
|
||||
case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS:
|
||||
*params = kFrontendMaxSubroutineUniformLocations;
|
||||
return;
|
||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||
*params = 15; // TODO
|
||||
return;
|
||||
@@ -1755,8 +1982,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_NUM_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_spirv_extensions / GL 4.6 core 22.2. An implementation that advertises no
|
||||
// SPIR-V extension answers zero here, and glGetStringi(GL_SPIR_V_EXTENSIONS, i) is then
|
||||
// never legally called - MobileGL runs the module through its own translation pipeline
|
||||
// and relies on no SPIR-V extension to do it, so zero is the true answer rather than a
|
||||
// placeholder.
|
||||
case GL_NUM_SPIR_V_EXTENSIONS:
|
||||
*params = 0;
|
||||
return;
|
||||
// GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
|
||||
// agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
|
||||
case GL_NUM_SHADER_BINARY_FORMATS:
|
||||
*params = 0; // ShaderBinary entrypoints are stubbed
|
||||
*params = 1;
|
||||
return;
|
||||
case GL_SHADER_BINARY_FORMATS:
|
||||
*params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
|
||||
return;
|
||||
case GL_PACK_ALIGNMENT:
|
||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
|
||||
@@ -1815,6 +2055,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PRIMITIVE_RESTART_INDEX:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPrimitiveRestartIndex());
|
||||
return;
|
||||
case GL_POLYGON_OFFSET_CLAMP:
|
||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 4.6
|
||||
// core 22.1's float-to-integer rule.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetPolygonOffsetClamp()));
|
||||
return;
|
||||
case GL_PROGRAM_BINARY_FORMATS:
|
||||
*params = 0; // program-binary entrypoints are stubbed
|
||||
return;
|
||||
@@ -1900,6 +2145,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_SAMPLE_MASK:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_SAMPLE_SHADING:
|
||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_MIN_SAMPLE_SHADING_VALUE:
|
||||
// GL 4.6 core 22.2: a floating-point value queried as an integer rounds to nearest.
|
||||
*params = static_cast<GLint>(std::lround(MG_State::pGLContext->GetMinSampleShadingValue()));
|
||||
return;
|
||||
case GL_SAMPLE_MASK_VALUE:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||
return;
|
||||
@@ -2118,7 +2370,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
case GL_MAX_ELEMENT_INDEX:
|
||||
*params = 1024 * 1024; // TODO
|
||||
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||
// state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE
|
||||
// uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT
|
||||
// indices and neither bounds an index value; the old `1024 * 1024` was a placeholder
|
||||
// that no draw path ever consulted.
|
||||
*params = INT32_MAX;
|
||||
return;
|
||||
case GL_CONTEXT_PROFILE_MASK:
|
||||
// Reports the requested context profile (EGL defaults 3.x contexts to core);
|
||||
@@ -2174,8 +2431,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
break;
|
||||
case GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS:
|
||||
// The CLAMPED block count, i.e. exactly what GL_MAX_COMPUTE_UNIFORM_BLOCKS answers.
|
||||
// GL 4.6 table 23.64 defines this as the components reachable through the blocks a
|
||||
// stage may declare, so deriving it from the raw backend number described 256 blocks
|
||||
// an application is only ever allowed 84 of.
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxComputeUniformComponents,
|
||||
dynamicParameters.MaxComputeUniformBlocks,
|
||||
ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS:
|
||||
@@ -2219,16 +2480,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
||||
break;
|
||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedColorTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||
kFrontendMaxFragmentUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxGeometryUniformComponents,
|
||||
kFrontendMaxGeometryUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_GEOMETRY_OUTPUT_VERTICES:
|
||||
@@ -2242,14 +2503,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxVertexUniformComponents,
|
||||
kFrontendMaxVertexUniformBlocks,
|
||||
ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks),
|
||||
dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||
break;
|
||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedDepthTextureMaxSamples();
|
||||
break;
|
||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||
*params = dynamicParameters.MaxFramebufferWidth;
|
||||
@@ -2276,7 +2537,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||
break;
|
||||
case GL_MAX_INTEGER_SAMPLES:
|
||||
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
||||
*params = GetAdvertisedIntegerMaxSamples();
|
||||
break;
|
||||
case GL_MAX_RENDERBUFFER_SIZE:
|
||||
*params = dynamicParameters.MaxRenderbufferSize;
|
||||
@@ -2287,12 +2548,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_PATCH_VERTICES:
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
|
||||
break;
|
||||
// Float state, so glGetIntegerv rounds it (GL 4.6 core 2.2.2) - the exact values come back
|
||||
// through glGetFloatv. Answered here so glGetBooleanv, which delegates to this getter for
|
||||
// everything its own switch does not handle, does not report INVALID_ENUM for them.
|
||||
case GL_PATCH_DEFAULT_OUTER_LEVEL: {
|
||||
const FloatVec4& outer = MG_State::pGLContext->GetPatchDefaultOuterLevel();
|
||||
for (Uint i = 0; i < 4; ++i) params[i] = static_cast<GLint>(std::lround(outer[i]));
|
||||
break;
|
||||
}
|
||||
case GL_PATCH_DEFAULT_INNER_LEVEL: {
|
||||
const FloatVec2& inner = MG_State::pGLContext->GetPatchDefaultInnerLevel();
|
||||
for (Uint i = 0; i < 2; ++i) params[i] = static_cast<GLint>(std::lround(inner[i]));
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch.
|
||||
// GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and
|
||||
// the DirectVulkan draw path relies on this staying false (it resolves primitive restart
|
||||
// to "never" for a PATCH_LIST topology on the strength of it).
|
||||
case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED:
|
||||
*params = GL_FALSE;
|
||||
break;
|
||||
case GL_MAX_PATCH_VERTICES:
|
||||
*params = dynamicParameters.MaxPatchVertices;
|
||||
break;
|
||||
case GL_MAX_TESS_GEN_LEVEL:
|
||||
*params = dynamicParameters.MaxTessGenLevel;
|
||||
break;
|
||||
// Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_
|
||||
// COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the
|
||||
// conformance suite's own formula exactly, so the two cannot drift.
|
||||
case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS:
|
||||
*params = GetMaxCombinedUniformComponents(
|
||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS),
|
||||
ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks), dynamicParameters.MaxUniformBlockSize);
|
||||
break;
|
||||
// ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and
|
||||
// for a stronger reason: a cull distance discards the whole primitive, so advertising
|
||||
// eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is
|
||||
// the honest answer on a host with no cull-distance route, and the conformance suite then
|
||||
// skips the functional cases instead of failing them deep inside a pixel comparison.
|
||||
case GL_MAX_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCullDistances;
|
||||
break;
|
||||
case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES:
|
||||
*params = dynamicParameters.MaxCombinedClipAndCullDistances;
|
||||
break;
|
||||
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||
*params = dynamicParameters.MinProgramTextureGatherOffset;
|
||||
break;
|
||||
@@ -2343,7 +2648,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
|
||||
break;
|
||||
case GL_MAX_VERTEX_STREAMS:
|
||||
*params = 1;
|
||||
// ONE, which is under the GL 4.5 core table 23.62 minimum of four and is a known,
|
||||
// deliberate non-conformance. It was briefly raised to 4 on the theory that streams
|
||||
// 1..3 could exist and be permanently empty; measuring that decision refuted it.
|
||||
// Raising the limit un-gates two CTS cases per package across KHR-GL40..GL46 -
|
||||
// transform_feedback.draw_xfb_stream_test (which stops being skipped) and
|
||||
// transform_feedback3.multiple_streams (which stops reporting NotSupported) - and
|
||||
// both then fail, because nothing in the shader pipeline supports layout(stream = N),
|
||||
// EmitStreamVertex or EndStreamPrimitive, and because the query state machine tracks
|
||||
// one active query per TARGET rather than per (target, stream). That is 14 new
|
||||
// failures against 2 gained limits passes, and a 4 nothing can back is the
|
||||
// advertised-caps lie with the sign flipped.
|
||||
//
|
||||
// The real fix is the feature, not the number: per-stream capture needs
|
||||
// layout(stream = N) through the transpiler plus per-(target, stream) query slots,
|
||||
// which DirectVulkan could back with VK_EXT_transform_feedback's geometryStreams and
|
||||
// DirectGLES cannot back at all (ES has no vertex streams). Until that lands, one is
|
||||
// the honest count and every stream-addressing entry point bounds itself by THIS
|
||||
// query, so raising it later moves them all together.
|
||||
*params = kFrontendMaxVertexStreams;
|
||||
break;
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||
@@ -2360,15 +2683,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_MAX_TEXTURE_SIZE:
|
||||
*params = dynamicParameters.MaxTextureSize;
|
||||
break;
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS:
|
||||
case GL_MAX_UNIFORM_BUFFER_BINDINGS: {
|
||||
// Never advertise more bindings than the state layer's indexed-binding array can track
|
||||
// (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that
|
||||
// capacity, and the GL CTS per-case state reset calls glBindBufferBase on every
|
||||
// advertised index and expects no error. The floor equals the GL 3.3 core minimum
|
||||
// (36), so the clamp never under-advertises.
|
||||
// advertised index and expects no error. The floor is the GL 4.5 core minimum, and
|
||||
// the array was widened to exactly it, so the two coincide by construction.
|
||||
//
|
||||
// WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage
|
||||
// family. A GL uniform binding point is where an APPLICATION parks a buffer; it is
|
||||
// not a driver binding point. Neither backend forwards it as one on the draw path:
|
||||
// DirectGLES rebinds the blocks a program declares onto COMPACTED ES points
|
||||
// (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan
|
||||
// resolves each block to a descriptor. So what the host driver's count bounds is how
|
||||
// many blocks ONE PROGRAM may use, not how many points an application may bind.
|
||||
//
|
||||
// That per-program number is NOT GL_MAX_COMBINED_UNIFORM_BLOCKS (84, the six-stage
|
||||
// sum): no single program can reach it. A graphics program is bounded by the five
|
||||
// graphics stages' per-stage counts, 14 each, so 70 blocks plus the global UBO at ES
|
||||
// point 0 = 71 - inside the ES 3.2 minimum of 72. A compute program is bounded by
|
||||
// GL_MAX_COMPUTE_UNIFORM_BLOCKS, which on DirectGLES is the ES driver's own count
|
||||
// (GL-scale, ~14) and on DirectVulkan is served from descriptors with no ES binding
|
||||
// points involved. Raising any per-stage graphics count past 14 is what would break
|
||||
// this, so that is the edit to check against the ES ceiling - not this one.
|
||||
static_assert(static_cast<GLint>(MG_State::GLState::BufferBindingPointCount) >=
|
||||
kFrontendMinUniformBufferBindings,
|
||||
"the indexed-binding array must be able to hold every advertised uniform binding point");
|
||||
*params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings,
|
||||
static_cast<GLint>(MG_State::GLState::BufferBindingPointCount));
|
||||
break;
|
||||
}
|
||||
case GL_MAX_UNIFORM_BLOCK_SIZE:
|
||||
*params = dynamicParameters.MaxUniformBlockSize;
|
||||
break;
|
||||
|
||||
@@ -25,7 +25,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
|
||||
// core minimum. Frontend multisample validators have to honour this ceiling for every
|
||||
// format, otherwise MobileGL rejects a sample count it advertised itself.
|
||||
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
|
||||
// ceilings below have a minimum of one and are reported as probed.
|
||||
GLint GetAdvertisedMaxSamples();
|
||||
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
|
||||
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
|
||||
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
|
||||
// the query promised - it used to floor both at 4 and then let the backend quietly
|
||||
// under-allocate whatever the driver could not actually provide.
|
||||
GLint GetAdvertisedColorTextureMaxSamples();
|
||||
GLint GetAdvertisedDepthTextureMaxSamples();
|
||||
GLint GetAdvertisedIntegerMaxSamples();
|
||||
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
|
||||
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
|
||||
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
|
||||
//
|
||||
// Shared rather than duplicated because two callers need the identical number and disagreeing
|
||||
// would be a silent bug: the query itself, and the draw path's write of the reserved
|
||||
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
|
||||
// is exactly what the sample_variables CTS does.
|
||||
GLint ResolveDrawFramebufferSampleCount();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "Config.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -30,10 +32,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
static bool CheckShaderNameValidity(Uint shader) {
|
||||
if (shader == 0 || !MG_State::pGLContext->ValidateShaderName(shader)) {
|
||||
// The mirror of CheckProgramNameValidity below, and for the same reason: programs and
|
||||
// shaders are drawn from ONE name space (ProgramState hands both out of a single
|
||||
// generator), so a name that exists but belongs to a PROGRAM is the wrong kind of
|
||||
// object - GL 3.3 core 2.11.x makes that INVALID_OPERATION - while a name GL never
|
||||
// handed out is INVALID_VALUE. This half of the split was missing, so every shader
|
||||
// entry point handed a program name reported INVALID_VALUE; the conformance suite
|
||||
// reads exactly that code back from glSpecializeShader.
|
||||
const ErrorCode error = (shader != 0 && MG_State::pGLContext->ValidateProgramName(shader))
|
||||
? ErrorCode::InvalidOperation
|
||||
: ErrorCode::InvalidValue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
error,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shader) + " is not a valid name."));
|
||||
std::to_string(shader) +
|
||||
(error == ErrorCode::InvalidOperation ? " is not a shader object."
|
||||
: " is not a valid name.")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -245,6 +259,30 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 7.6.3: INVALID_VALUE when uniformBlockBinding >= MAX_UNIFORM_BUFFER_BINDINGS.
|
||||
// The storage-block twin below has always had this check; the uniform one never did, and the
|
||||
// value it stores is used as a RAW SUBSCRIPT into the state layer's fixed indexed-binding
|
||||
// array on every draw and dispatch (DirectGLES's per-program UBO rebind, DirectVulkan's
|
||||
// descriptor resolve, whose only guard is a MOBILEGL_ASSERT that compiles away in release).
|
||||
// An out-of-range binding therefore did not merely go unreported - it read past the array and
|
||||
// dereferenced whatever SharedPtr it found there.
|
||||
bool ValidateUniformBlockBinding(GLuint binding) {
|
||||
// Exactly what glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS) advertises: the state
|
||||
// layer's array width, which the getter clamps to as well.
|
||||
const SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::Uniform);
|
||||
if (binding < maxBindingCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("Uniform block binding {} is not less than GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).", binding,
|
||||
maxBindingCount)));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateShaderStorageBlockBinding(GLuint binding) {
|
||||
SizeT maxBindingCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::ShaderStorage);
|
||||
if (MG_Backend::pActiveBackendObject) {
|
||||
@@ -307,9 +345,195 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompileShader_State(GLuint shader) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
// ARB_gl_spirv: "INVALID_OPERATION is generated by CompileShader if shader has been
|
||||
// associated with a SPIR-V binary". Such an object has no GLSL source to compile - it is
|
||||
// waiting for glSpecializeShader, which is the operation that compiles it.
|
||||
if (shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" holds a SPIR-V binary; use glSpecializeShader instead of glCompileShader."));
|
||||
return;
|
||||
}
|
||||
shaderObject->Compile();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// GL_ARB_gl_spirv
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
void ShaderBinary_State(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary,
|
||||
GLsizei length) {
|
||||
if (count < 0 || length < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count and length must be non-negative."));
|
||||
return;
|
||||
}
|
||||
// GL_NUM_SHADER_BINARY_FORMATS advertises exactly one format, so every other value is
|
||||
// INVALID_ENUM (GL 4.6 core 7.2). This is the check that used to be missing entirely -
|
||||
// the entry point was a silent stub, so an application handed a format nothing supports
|
||||
// and was told nothing.
|
||||
if (binaryformat != GL_SHADER_BINARY_FORMAT_SPIR_V) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"binaryformat must be GL_SHADER_BINARY_FORMAT_SPIR_V."));
|
||||
return;
|
||||
}
|
||||
if (count == 0) return;
|
||||
if (shaders == nullptr || (length > 0 && binary == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "shaders and binary must not be null."));
|
||||
return;
|
||||
}
|
||||
// A SPIR-V module is a sequence of 32-bit words, so a length that is not a multiple of
|
||||
// four cannot be one (ARB_gl_spirv makes this INVALID_VALUE).
|
||||
if ((length % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"length must be a multiple of four for a SPIR-V module."));
|
||||
return;
|
||||
}
|
||||
|
||||
// EVERY name is validated before ANY of them is written: the entry point is all-or-
|
||||
// nothing, and half-applying it would leave some objects holding a module the call was
|
||||
// rejected for. The duplicate check is the extension's own ("INVALID_VALUE ... if the
|
||||
// same shader object is specified more than once").
|
||||
std::set<GLuint> seen;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!seen.insert(shaders[i]).second) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shaders[i]) +
|
||||
" appears more than once in `shaders`."));
|
||||
return;
|
||||
}
|
||||
if (!MG_State::pGLContext->ValidateShaderName(shaders[i])) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(shaders[i]) + " is not the name of a shader object."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT wordCount = static_cast<SizeT>(length) / 4;
|
||||
Vector<Uint32> module(wordCount);
|
||||
if (wordCount != 0) {
|
||||
Memcpy(module.data(), binary, static_cast<SizeT>(length));
|
||||
}
|
||||
// spirv-val here, not at glSpecializeShader: this is where the words arrive, and past it
|
||||
// they reach SPIRV-Cross, which parses rather than validates. ARB_gl_spirv lets an
|
||||
// implementation reject an invalid module at either call; rejecting at the earlier one
|
||||
// means the application's error is reported next to the data that caused it.
|
||||
if (const auto validated = MG_Util::ShaderTranspiler::ShaderCompiler::ValidateSpirvModule(module);
|
||||
!validated) {
|
||||
MGLOG_D("%s: rejected SPIR-V module: %s", __func__, validated.error().log.c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, validated.error().log));
|
||||
return;
|
||||
}
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
auto& shaderObject = TryToGetShaderObject(shaders[i]);
|
||||
if (!shaderObject) continue;
|
||||
// A copy per object, not a shared buffer: each shader object may be specialized with
|
||||
// different constants, and each specialization re-reads its own original words.
|
||||
Vector<Uint32> perObject = module;
|
||||
shaderObject->SetSpirvBinary(Move(perObject));
|
||||
}
|
||||
}
|
||||
|
||||
void SpecializeShader_State(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
if (!shaderObject->HasSpirvBinary()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has no SPIR-V binary; call glShaderBinary first."));
|
||||
return;
|
||||
}
|
||||
// ARB_gl_spirv: a shader that has already been specialized may not be specialized again
|
||||
// until glShaderBinary re-associates a module with it.
|
||||
if (shaderObject->HasBeenSpecialized()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"shader " + std::to_string(shader) +
|
||||
" has already been specialized; re-associate its module with "
|
||||
"glShaderBinary before specializing it again."));
|
||||
return;
|
||||
}
|
||||
// pEntryPoint names the entry point to specialize; there is no default. A null pointer
|
||||
// cannot name one, and neither can the empty string.
|
||||
if (pEntryPoint == nullptr || *pEntryPoint == '\0') {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pEntryPoint must name an entry point."));
|
||||
return;
|
||||
}
|
||||
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pConstantIndex and pConstantValue must not be null."));
|
||||
return;
|
||||
}
|
||||
// "INVALID_VALUE is generated if any value in pConstantIndex is repeated" - checked before
|
||||
// anything is applied, for the same all-or-nothing reason glShaderBinary checks its names
|
||||
// up front.
|
||||
Vector<Uint32> constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants);
|
||||
Vector<Uint32> constantValues(pConstantValue, pConstantValue + numSpecializationConstants);
|
||||
{
|
||||
std::set<Uint32> seen;
|
||||
for (const Uint32 id : constantIds) {
|
||||
if (seen.insert(id).second) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"constant index " + std::to_string(id) + " is repeated."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const String entryPoint(pEntryPoint);
|
||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
|
||||
using SpecializationFailure = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializationFailure;
|
||||
SpecializationFailure failure = SpecializationFailure::None;
|
||||
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
|
||||
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues, failure);
|
||||
if (!specialized) {
|
||||
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
|
||||
specialized.error().log.c_str());
|
||||
// The two conditions ARB_gl_spirv ENUMERATES are GL errors, and an erroring GL command
|
||||
// must have no other effect - so the shader object is left exactly as it was rather
|
||||
// than being pushed into a failed-compile state. Anything else is a genuine compile
|
||||
// failure of a well-formed request, which the extension routes through COMPILE_STATUS
|
||||
// and the info log exactly as glCompileShader does.
|
||||
if (failure == SpecializationFailure::UnknownEntryPoint ||
|
||||
failure == SpecializationFailure::UnknownConstantId) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
|
||||
return;
|
||||
}
|
||||
shaderObject->SpecializeFromSpirv(Move(specialized.value().glsl), Move(specialized.value().xfbVaryings),
|
||||
specialized.value().xfbBufferMode);
|
||||
}
|
||||
|
||||
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
|
||||
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
|
||||
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
|
||||
@@ -744,12 +968,77 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
case GL_PROGRAM_SEPARABLE:
|
||||
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
|
||||
// The LATCHED flag, not the live one: glProgramParameteri's write takes effect at the
|
||||
// next link (GL 4.6 core 7.3), so a program told to be separable and then never
|
||||
// linked still reports GL_FALSE.
|
||||
*params = programObject->GetLinkedSeparable() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
|
||||
// The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as
|
||||
// GL_COMPUTE_WORK_GROUP_SIZE above, and for the same reason: "a linked program object
|
||||
// with a geometry shader" is one whose EXECUTABLE has the stage, so an
|
||||
// attached-but-not-yet-linked shader must give INVALID_OPERATION rather than the previous
|
||||
// link's value. The geometry three used to be listed here only to fall through into the
|
||||
// INVALID_ENUM default, and the tessellation five were not listed at all.
|
||||
case GL_GEOMETRY_VERTICES_OUT:
|
||||
case GL_GEOMETRY_INPUT_TYPE:
|
||||
case GL_GEOMETRY_OUTPUT_TYPE:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a geometry shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_GEOMETRY_VERTICES_OUT: *params = programObject->GetGeometryVerticesOut(); break;
|
||||
case GL_GEOMETRY_INPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryInputType()); break;
|
||||
case GL_GEOMETRY_OUTPUT_TYPE: *params = static_cast<GLint>(programObject->GetGeometryOutputType()); break;
|
||||
default: *params = programObject->GetGeometryShaderInvocations(); break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_CONTROL_OUTPUT_VERTICES: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessControl)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation control shader."));
|
||||
return;
|
||||
}
|
||||
*params = programObject->GetTessControlOutputVertices();
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
case GL_TESS_GEN_MODE:
|
||||
case GL_TESS_GEN_SPACING:
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
case GL_TESS_GEN_POINT_MODE: {
|
||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::TessEval)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::to_string(program) +
|
||||
" is not a linked program object with a tessellation evaluation shader."));
|
||||
return;
|
||||
}
|
||||
switch (pname) {
|
||||
case GL_TESS_GEN_MODE: *params = static_cast<GLint>(programObject->GetTessGenMode()); break;
|
||||
case GL_TESS_GEN_SPACING: *params = static_cast<GLint>(programObject->GetTessGenSpacing()); break;
|
||||
case GL_TESS_GEN_VERTEX_ORDER:
|
||||
*params = static_cast<GLint>(programObject->GetTessGenVertexOrder());
|
||||
break;
|
||||
default: *params = programObject->GetTessGenPointMode() ? GL_TRUE : GL_FALSE; break;
|
||||
}
|
||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_D("%s: %s", __func__, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -811,8 +1100,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||
break;
|
||||
case GL_SHADER_SOURCE_LENGTH:
|
||||
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
|
||||
case GL_SHADER_SOURCE_LENGTH: {
|
||||
// The APPLICATION's source, which is empty for a shader that came from glShaderBinary -
|
||||
// see ShaderObject::GetApplicationShaderSource.
|
||||
const auto& source = shaderObject->GetApplicationShaderSource();
|
||||
*params = source.empty() ? 0 : (GLint)source.length() + 1;
|
||||
break;
|
||||
}
|
||||
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
|
||||
// while the object stands for an application-supplied module. It is the FIRST thing the
|
||||
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
|
||||
// default arm below and take the whole test with it.
|
||||
case GL_SPIR_V_BINARY:
|
||||
*params = shaderObject->HasSpirvBinary() ? GL_TRUE : GL_FALSE;
|
||||
break;
|
||||
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
|
||||
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
|
||||
@@ -858,13 +1158,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
|
||||
auto& src = shaderObject->GetShaderSource();
|
||||
auto& src = shaderObject->GetApplicationShaderSource();
|
||||
CopyStr(bufSize, length, source, src.c_str(), (GLsizei)src.length());
|
||||
}
|
||||
|
||||
GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return -1;
|
||||
// GL 4.6 core 7.6: "INVALID_OPERATION is generated if program has not been successfully
|
||||
// linked". Answering -1 silently is not the same thing - the conformance suite reads the
|
||||
// error, not the location.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return -1;
|
||||
}
|
||||
auto loc = programObject->GetUniformLocation(name);
|
||||
MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
|
||||
return loc;
|
||||
@@ -1277,11 +1587,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
template <GLsizei ItemCount, typename T>
|
||||
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
// The link check comes BEFORE the location == -1 early-out, not after. GL 4.6 core 7.6
|
||||
// makes an unlinked program INVALID_OPERATION regardless of the location, and -1 is
|
||||
// exactly the location an application holds after glGetUniformLocation on such a program -
|
||||
// so checking -1 first swallowed the very case the rule exists for.
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -1289,6 +1601,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
// "If location is equal to -1, the data passed in will be silently ignored and the
|
||||
// specified uniform variable will not be changed" - after the program itself has been
|
||||
// found acceptable.
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint offset = 0; offset < count; offset++) {
|
||||
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
|
||||
@@ -1699,8 +2015,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1712,14 +2026,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, __func__, location, count, transpose, value, 2, 2,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
|
||||
void ProgramUniformMatrix3fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1731,6 +2045,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1756,8 +2072,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4fv_State(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLfloat* value) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1769,6 +2083,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
for (GLint i = 0; i < count; i++) {
|
||||
if (i > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + i)) {
|
||||
// Values for elements beyond the end of the uniform array are ignored.
|
||||
@@ -1790,8 +2106,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrixNonSquarefv_State(const char* caller, GLuint program, GLint location, GLsizei count,
|
||||
GLboolean transpose, const GLfloat* value, Int columns, Int rows) {
|
||||
if (location == -1) return;
|
||||
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
|
||||
@@ -1803,6 +2117,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location == -1) return;
|
||||
|
||||
UniformMatrixfv_Object(*programObject, caller, location, count, transpose, value, columns, rows,
|
||||
"program " + std::to_string(program));
|
||||
}
|
||||
@@ -1836,6 +2152,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Program object" + std::to_string(program) + " that has been linked."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateUniformBlockBinding(uniformBlockBinding)) return;
|
||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
@@ -2083,6 +2400,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
BindAttribLocation_State(program, index, name);
|
||||
}
|
||||
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) {
|
||||
ShaderBinary_State(count, shaders, binaryformat, binary, length);
|
||||
}
|
||||
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
|
||||
SpecializeShader_State(shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue);
|
||||
}
|
||||
|
||||
void CompileShader(GLuint shader) {
|
||||
CompileShader_State(shader);
|
||||
}
|
||||
@@ -2342,7 +2668,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2352,6 +2677,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 2);
|
||||
}
|
||||
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2368,7 +2694,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2378,6 +2703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 3);
|
||||
}
|
||||
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2394,7 +2720,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2404,6 +2729,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 4);
|
||||
}
|
||||
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2420,7 +2746,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2430,6 +2755,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 3);
|
||||
}
|
||||
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2446,7 +2772,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2456,6 +2781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 2, 4);
|
||||
}
|
||||
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2472,7 +2798,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2482,6 +2807,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 2);
|
||||
}
|
||||
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2498,7 +2824,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2508,6 +2833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 3, 4);
|
||||
}
|
||||
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2524,7 +2850,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2534,6 +2859,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 2);
|
||||
}
|
||||
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) {
|
||||
@@ -2550,7 +2876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value) {
|
||||
if (location == -1) return;
|
||||
auto& programObject = TryToGetProgramObject(program);
|
||||
if (!programObject) return;
|
||||
if (!programObject->GetLinkStatus()) {
|
||||
@@ -2560,6 +2885,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"program " + std::to_string(program) + " is not linked."));
|
||||
return;
|
||||
}
|
||||
if (location == -1) return;
|
||||
UniformMatrixdv_Object(*programObject, location, count, transpose, value, 4, 3);
|
||||
}
|
||||
void GetUniformdv(GLuint program, GLint location, GLdouble* params) {
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void AttachShader(GLuint program, GLuint shader);
|
||||
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
|
||||
void CompileShader(GLuint shader);
|
||||
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
|
||||
// the module to one or more shader objects, glSpecializeShader names its entry point and
|
||||
// supplies its specialization constants and is what actually compiles them.
|
||||
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
|
||||
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
|
||||
const GLuint* pConstantIndex, const GLuint* pConstantValue);
|
||||
GLuint CreateProgram(void);
|
||||
GLuint CreateShader(GLenum type);
|
||||
void DeleteProgram(GLuint program);
|
||||
|
||||
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
std::format("Program {} has not been linked successfully.", program));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
|
||||
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
|
||||
// whose live flag was cleared after a separable link is still a legal stage, and a
|
||||
// program whose live flag was set after a non-separable link is not.
|
||||
if (!programObject->GetLinkedSeparable()) {
|
||||
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||
std::format("Program {} was not linked as a separable program.", program));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
|
||||
|
||||
@@ -59,6 +59,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLuint g_activePrimitivesGeneratedQueryId = 0;
|
||||
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
|
||||
GLuint g_activeSamplesPassedQueryId = 0;
|
||||
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
|
||||
// target (0 = none). A map rather than a field per target: the eleven behave identically
|
||||
// and none of them has any state beyond "which object is counting".
|
||||
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
|
||||
|
||||
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
|
||||
// A 4.6 core context has to ACCEPT all of them at glBeginQuery - the extension is core
|
||||
// since 4.6 and there is no query by which an application could learn otherwise before
|
||||
// calling. MobileGL instruments none of them, and says so the way GL 4.6 core 4.2.1
|
||||
// provides for: GL_QUERY_COUNTER_BITS answers zero for these targets, which is the
|
||||
// spec's own signal that the counter is unsupported and its results indeterminate. That
|
||||
// is an honest zero, not an advertised capability - the alternative, GL_INVALID_ENUM on a
|
||||
// core entry point, is both non-conformant AND less informative.
|
||||
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
|
||||
switch (target) {
|
||||
case GL_VERTICES_SUBMITTED:
|
||||
case GL_PRIMITIVES_SUBMITTED:
|
||||
case GL_VERTEX_SHADER_INVOCATIONS:
|
||||
case GL_TESS_CONTROL_SHADER_PATCHES:
|
||||
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_INVOCATIONS:
|
||||
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
|
||||
case GL_FRAGMENT_SHADER_INVOCATIONS:
|
||||
case GL_COMPUTE_SHADER_INVOCATIONS:
|
||||
case GL_CLIPPING_INPUT_PRIMITIVES:
|
||||
case GL_CLIPPING_OUTPUT_PRIMITIVES:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Bool TimerQueryDisabled() {
|
||||
return MG_Config::Features.DisableTimerQuery;
|
||||
@@ -370,6 +401,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
queryObject->active = false;
|
||||
g_activeSamplesPassedQueryId = 0;
|
||||
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
|
||||
queryObject->active = false;
|
||||
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
|
||||
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
|
||||
queryObject->target == GL_PRIMITIVES_GENERATED) {
|
||||
queryObject->active = false;
|
||||
@@ -410,7 +444,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
|
||||
// need backend support.
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
@@ -426,10 +462,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
|
||||
return;
|
||||
}
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId != 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||
"A query is already active on this target.");
|
||||
@@ -448,7 +486,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ResetQueryObjectLocked(queryObject); // discard any previous result
|
||||
queryObject->target = target;
|
||||
queryObject->active = true;
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
|
||||
// The object still becomes a real, target-latched query so every other rule about it
|
||||
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
|
||||
} else if (isTransformFeedbackQuery) {
|
||||
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
|
||||
// the CPU accounting delta stays as the fallback when the backend lacks them.
|
||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||
@@ -476,15 +518,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
|
||||
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
|
||||
const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
|
||||
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
|
||||
!isPipelineStatisticsQuery) {
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
GLuint& activeQueryId = isTransformFeedbackQuery
|
||||
GLuint& activeQueryId = isPipelineStatisticsQuery
|
||||
? g_activePipelineStatisticsQueryIds[target]
|
||||
: (isTransformFeedbackQuery
|
||||
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
|
||||
: g_activePrimitivesGeneratedQueryId)
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
|
||||
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
|
||||
if (activeQueryId == 0) {
|
||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
|
||||
return;
|
||||
@@ -494,6 +540,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
activeQueryId = 0; // should not happen; keep state consistent
|
||||
return;
|
||||
}
|
||||
if (isPipelineStatisticsQuery) {
|
||||
// The result is a definite zero rather than an unread backend handle, so a later
|
||||
// GetQueryObject* answers immediately and never waits on something that was never
|
||||
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
|
||||
queryObject->cachedResult = 0;
|
||||
queryObject->resultCached = true;
|
||||
queryObject->active = false;
|
||||
queryObject->ended = true;
|
||||
activeQueryId = 0;
|
||||
return;
|
||||
}
|
||||
if (isTransformFeedbackQuery) {
|
||||
if (queryObject->backendHandle) {
|
||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||
@@ -657,7 +714,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
|
||||
break;
|
||||
default:
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
const auto it = g_activePipelineStatisticsQueryIds.find(target);
|
||||
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
|
||||
} else {
|
||||
*params = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
@@ -668,6 +730,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// entry points / timestamp valid bits at call time, not at table
|
||||
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
|
||||
// wins.
|
||||
if (IsPipelineStatisticsQueryTarget(target)) {
|
||||
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
|
||||
// results are indeterminate. The conformance suite reads exactly this and skips
|
||||
// the functional half of each such target, which is the outcome an uninstrumented
|
||||
// counter should produce.
|
||||
*params = 0;
|
||||
return;
|
||||
}
|
||||
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
|
||||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
|
||||
@@ -741,14 +811,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsPerVertexStreamQueryTarget(GLenum target) {
|
||||
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
}
|
||||
|
||||
// The indexed query entry points differ from the plain ones only in the vertex
|
||||
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
|
||||
// for the two transform feedback targets and zero for every other target. With a
|
||||
// single vertex stream both bounds are 1, so a valid call is always index 0 and
|
||||
// forwards to the unindexed implementation.
|
||||
// for the two transform feedback targets and zero for every other target. MobileGL
|
||||
// implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
|
||||
// which is what makes the three forwards below equivalent to the unindexed entry points.
|
||||
//
|
||||
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
|
||||
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
|
||||
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
|
||||
// per-TARGET globals and would end - or report - a query begun on a different stream.
|
||||
// Raising that limit therefore means giving each active query a stream index and
|
||||
// comparing it here, not just changing the number.
|
||||
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
|
||||
const Bool perStreamTarget =
|
||||
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||
const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
|
||||
GLint maxVertexStreams = 1;
|
||||
if (perStreamTarget) {
|
||||
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||
@@ -761,6 +841,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
: "index must be zero for this query target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
|
||||
|
||||
@@ -328,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
|
||||
}
|
||||
|
||||
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
|
||||
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
|
||||
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
|
||||
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
|
||||
void MinSampleShading_State(GLfloat value) {
|
||||
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void PolygonOffset_State(GLfloat factor, GLfloat units) {
|
||||
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
|
||||
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
|
||||
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
|
||||
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
|
||||
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
|
||||
static_cast<Float>(clamp));
|
||||
}
|
||||
|
||||
void ClipControl_State(GLenum origin, GLenum depth) {
|
||||
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
|
||||
// GL_INVALID_ENUM with the state left untouched.
|
||||
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
|
||||
MG_Util::ConvertGLEnumToString(origin) + "."));
|
||||
return;
|
||||
}
|
||||
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
|
||||
MG_Util::ConvertGLEnumToString(depth) + "."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->SetClipControl(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode_State(GLenum face, GLenum mode) {
|
||||
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
|
||||
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
|
||||
@@ -1013,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
SampleCoverage_State(value, invert);
|
||||
}
|
||||
|
||||
void MinSampleShading(GLfloat value) {
|
||||
MinSampleShading_State(value);
|
||||
}
|
||||
|
||||
void PolygonOffset(GLfloat factor, GLfloat units) {
|
||||
PolygonOffset_State(factor, units);
|
||||
}
|
||||
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
|
||||
PolygonOffsetClamp_State(factor, units, clamp);
|
||||
}
|
||||
|
||||
void ClipControl(GLenum origin, GLenum depth) {
|
||||
ClipControl_State(origin, depth);
|
||||
}
|
||||
|
||||
void PolygonMode(GLenum face, GLenum mode) {
|
||||
PolygonMode_State(face, mode);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void StencilFunc(GLenum func, GLint ref, GLuint mask);
|
||||
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
void SampleCoverage(GLfloat value, GLboolean invert);
|
||||
void MinSampleShading(GLfloat value);
|
||||
void PolygonOffset(GLfloat factor, GLfloat units);
|
||||
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
|
||||
void ClipControl(GLenum origin, GLenum depth);
|
||||
void PolygonMode(GLenum face, GLenum mode);
|
||||
void PointSize(GLfloat size);
|
||||
void PointParameterf(GLenum pname, GLfloat param);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
namespace {
|
||||
@@ -22,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return static_cast<Float>(*(const GLint*)param);
|
||||
}
|
||||
|
||||
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
|
||||
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
|
||||
// reads exactly one component and does not care.
|
||||
Bool IsVectorOnlySamplerPname(GLenum pname) {
|
||||
return pname == GL_TEXTURE_BORDER_COLOR;
|
||||
}
|
||||
|
||||
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
|
||||
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
|
||||
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
|
||||
// sampler path funnels all three spellings through one void* function, which is precisely how
|
||||
// it came to write a fixed type regardless of the caller.
|
||||
//
|
||||
// Truncation rather than rounding for the float -> integer direction, matching the texture
|
||||
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
|
||||
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
|
||||
// sampler queried the same way must answer the same number.
|
||||
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = value;
|
||||
return;
|
||||
}
|
||||
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
|
||||
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
|
||||
// conversion is the well-defined modular one, and it is what the texture-side
|
||||
// GetTexParameterIuiv fallback does.
|
||||
const GLint asInt = static_cast<GLint>(value);
|
||||
if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(asInt);
|
||||
} else {
|
||||
*(GLint*)params = asInt;
|
||||
}
|
||||
}
|
||||
|
||||
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = static_cast<GLfloat>(value);
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = value;
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(value);
|
||||
}
|
||||
}
|
||||
|
||||
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
|
||||
if (param == nullptr) return false;
|
||||
|
||||
@@ -56,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
|
||||
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
|
||||
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
|
||||
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
|
||||
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
|
||||
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
|
||||
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
|
||||
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (param == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -112,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isFloat) {
|
||||
const auto* values = (const GLfloat*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
|
||||
} else if (!isIntegerCommand) {
|
||||
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
|
||||
const auto* values = (const GLint*)param;
|
||||
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto* values = (const GLuint*)param;
|
||||
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
|
||||
@@ -128,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
|
||||
bool isUnsignedInteger) {
|
||||
bool isUnsignedInteger, bool isIntegerCommand) {
|
||||
if (params == nullptr) return;
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
|
||||
@@ -141,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
|
||||
|
||||
using namespace MG_Util;
|
||||
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
|
||||
// decides the destination type. Writing a fixed type regardless - which is what these case
|
||||
// labels used to do - hands back the other type's bit pattern rather than a converted value:
|
||||
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
|
||||
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
|
||||
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
|
||||
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
|
||||
// how the same bug class was already found and fixed once for a single pname.
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_WRAP_S:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_T:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
|
||||
break;
|
||||
case GL_TEXTURE_WRAP_R:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
|
||||
samplerObj->GetMipmapMode()));
|
||||
break;
|
||||
case GL_TEXTURE_MAG_FILTER:
|
||||
*(GLuint*)params =
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None);
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
|
||||
SamplerMipmapMode::None));
|
||||
break;
|
||||
case GL_TEXTURE_MIN_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMinLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_LOD:
|
||||
*(GLfloat*)params = samplerObj->GetMaxLod();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
|
||||
break;
|
||||
case GL_TEXTURE_LOD_BIAS:
|
||||
*(GLfloat*)params = samplerObj->GetLodBias();
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
|
||||
break;
|
||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||
if (isFloat) {
|
||||
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
|
||||
} else if (isUnsignedInteger) {
|
||||
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
|
||||
} else {
|
||||
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
|
||||
}
|
||||
StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
|
||||
break;
|
||||
case GL_TEXTURE_COMPARE_FUNC:
|
||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
|
||||
StoreSamplerEnum(params, isFloat, isUnsignedInteger,
|
||||
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
if (isFloat) {
|
||||
@@ -191,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
out[1] = color.y();
|
||||
out[2] = color.z();
|
||||
out[3] = color.w();
|
||||
} else if (!isIntegerCommand) {
|
||||
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
|
||||
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
|
||||
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
|
||||
const auto& color = samplerObj->GetBorderColor();
|
||||
auto* out = (GLint*)params;
|
||||
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
|
||||
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
|
||||
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
|
||||
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
|
||||
} else if (isUnsignedInteger) {
|
||||
const auto& color = samplerObj->GetBorderColorUI();
|
||||
auto* out = (GLuint*)params;
|
||||
@@ -293,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (sampler == 0) {
|
||||
textureUnit.SetSamplerObject(nullptr);
|
||||
} else {
|
||||
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same
|
||||
// name, which is why this cannot go through the shared SamplerImpl validator.
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return;
|
||||
}
|
||||
// GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
|
||||
// deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
|
||||
// name, so the shared validator answers for all of them.
|
||||
if (!SamplerImpl::ValidateSamplerName(sampler)) return;
|
||||
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
|
||||
if (!doesSamplerObjectCreated) {
|
||||
MG_State::pGLContext->CreateSamplerObject(sampler);
|
||||
@@ -356,30 +428,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, true);
|
||||
SetSamplerParam_State(sampler, pname, param, false, true, true);
|
||||
}
|
||||
|
||||
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, true);
|
||||
}
|
||||
|
||||
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, false, false);
|
||||
SetSamplerParam_State(sampler, pname, param, false, false, false);
|
||||
}
|
||||
|
||||
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
|
||||
SetSamplerParam_State(sampler, pname, param, true, false);
|
||||
SetSamplerParam_State(sampler, pname, param, true, false, false);
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
|
||||
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
|
||||
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
|
||||
// past the caller's single stack scalar - taking the address of a by-value argument and handing
|
||||
// it to a four-component reader is what these used to do. The texture-side twins already answer
|
||||
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
|
||||
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameteriv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
|
||||
if (IsVectorOnlySamplerPname(pname)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
|
||||
"pname has more than one component and needs a vector form."));
|
||||
return;
|
||||
}
|
||||
SamplerParameterfv(sampler, pname, ¶m);
|
||||
}
|
||||
|
||||
@@ -388,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, true);
|
||||
GetSamplerParam_State(sampler, pname, params, false, true, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, false, false);
|
||||
GetSamplerParam_State(sampler, pname, params, false, false, true);
|
||||
}
|
||||
|
||||
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
|
||||
GetSamplerParam_State(sampler, pname, params, true, false);
|
||||
GetSamplerParam_State(sampler, pname, params, true, false, false);
|
||||
}
|
||||
|
||||
void GenSamplers(GLsizei count, GLuint* samplers) {
|
||||
|
||||
@@ -12,10 +12,16 @@
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
||||
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
|
||||
// sampler object previously returned from a call to GenSamplers." That class is shared by every
|
||||
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
|
||||
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
|
||||
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
|
||||
Bool ValidateSamplerName(GLuint sampler) {
|
||||
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
|
||||
std::format("Invalid sampler name {}", sampler)));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/Validators.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Sampler/Validators.h>
|
||||
#include <MG_Util/Math/FixedPointConversion.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
@@ -41,13 +43,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->SetBorderColor(FloatVec4(params[0], params[1], params[2], params[3]));
|
||||
}
|
||||
|
||||
// glTexParameteriv(GL_TEXTURE_BORDER_COLOR): GL 4.6 core 8.10 sends the components through
|
||||
// equation 2.2 into the floating-point border colour. glGetTexParameteriv reverses it with
|
||||
// equation 2.3; the two live in one header so they cannot drift apart.
|
||||
void SetTextureBorderColorFromInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const GLint* params) {
|
||||
constexpr Float kSignedIntToFloat = 1.0f / 2147483647.0f;
|
||||
textureObject->SetBorderColor(FloatVec4(static_cast<Float>(params[0]) * kSignedIntToFloat,
|
||||
static_cast<Float>(params[1]) * kSignedIntToFloat,
|
||||
static_cast<Float>(params[2]) * kSignedIntToFloat,
|
||||
static_cast<Float>(params[3]) * kSignedIntToFloat));
|
||||
textureObject->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(params[0]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(params[1]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(params[2]),
|
||||
MG_Util::SignedNormalizedInt32ToFloat(params[3])));
|
||||
}
|
||||
|
||||
void SetTextureBorderColorFromIntegerInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
@@ -339,6 +343,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false);
|
||||
case GL_TEXTURE_DEPTH_TYPE:
|
||||
return GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false);
|
||||
case GL_TEXTURE_SHARED_SIZE:
|
||||
// GL 4.6 core table 8.24: the size in bits of the SHARED EXPONENT, which only the
|
||||
// one shared-exponent format has. Everything else answers zero, and the
|
||||
// conformance suite compares "at least", not "equal".
|
||||
return textureInternalFormat == TextureInternalFormat::RGB9E5 ? 5 : 0;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname);
|
||||
return 0;
|
||||
@@ -398,10 +407,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
|
||||
}
|
||||
|
||||
// Array targets store their layer count in z; layers never participate in mip
|
||||
// reduction (GL 3.3 §3.8.14), only true 3D textures halve their depth per level.
|
||||
// How many components of a GL-space texel size actually halve down the mip chain.
|
||||
//
|
||||
// An array texture's LAYER COUNT is not a dimension of the image (GL 4.6 core 8.14.3): it
|
||||
// stays put all the way down, and it is stored in whichever component sits after the
|
||||
// image's own dimensions - z for a 2D array or a cube array, and HEIGHT for a 1D array,
|
||||
// whose level is recorded as {width, layers, 1}.
|
||||
//
|
||||
// THE one statement of that rule on the frontend side, because three readers have to agree
|
||||
// on it or a chain is allocated under one and judged under another: this allocator,
|
||||
// ComputeMipmapCompleteForFilter (MG_State/GLState/TextureState/TextureObject.cpp, which
|
||||
// uses the identical 1/2/3 split) and DirectVulkan's MipShrinkingComponentCount. It used to
|
||||
// be a two-way `depthMips` flag, which had no way to say "height is not a dimension" - so
|
||||
// glGenerateMipmap on a GL_TEXTURE_1D_ARRAY allocated a chain whose LAYER COUNT halved,
|
||||
// and the completeness rule then rejected the texture the generate was supposed to make
|
||||
// complete. The backend allocator could not repair it either: it only ever GROWS a chain,
|
||||
// and the frontend's (wrong) count is always the longer of the two.
|
||||
Int MipShrinkingAxisCount(TextureTarget target) {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
// {width, 1, 1} - the other two are already 1, but say so rather than rely on it.
|
||||
return 1;
|
||||
case TextureTarget::Texture1DArray:
|
||||
// {width, layers, 1}: height IS the layer count.
|
||||
return 1;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
// {width, height, layers}: depth IS the layer count.
|
||||
return 2;
|
||||
case TextureTarget::Texture3D:
|
||||
return 3;
|
||||
default:
|
||||
// 2D, cube faces, rectangle, multisample: a plain two-dimensional image.
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Only true 3D textures halve their depth per level; every array target keeps its layer
|
||||
// count. Expressed through the rule above so the two cannot drift.
|
||||
Bool DepthParticipatesInMipmapping(TextureTarget target) {
|
||||
return target == TextureTarget::Texture3D;
|
||||
return MipShrinkingAxisCount(target) == 3;
|
||||
}
|
||||
|
||||
// Which targets each glTextureStorage*D accepts (GL 4.6 core 8.19). A texture whose target
|
||||
@@ -422,20 +467,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The longest mip chain the level-0 size admits. A 1D array keeps its layer count in
|
||||
// height, so unlike a 2D texture its height takes no part in the reduction.
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips);
|
||||
// The longest mip chain the level-0 size admits, over the axes that actually reduce.
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);
|
||||
|
||||
Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
|
||||
const Int mipHeight = (target == TextureTarget::Texture1DArray) ? 1 : std::max<Int>(height, 1);
|
||||
return ComputeFullMipmapLevelCount({std::max<Int>(width, 1), mipHeight, std::max<Int>(depth, 1)},
|
||||
DepthParticipatesInMipmapping(target));
|
||||
return ComputeFullMipmapLevelCount(
|
||||
{std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
|
||||
MipShrinkingAxisCount(target));
|
||||
}
|
||||
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Bool depthMips) {
|
||||
Int maxDimension = std::max<Int>(
|
||||
baseTexelSize.x(),
|
||||
std::max<Int>(baseTexelSize.y(), depthMips ? std::max<Int>(baseTexelSize.z(), 1) : 1));
|
||||
Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
|
||||
Int maxDimension = 1;
|
||||
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
|
||||
maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
|
||||
}
|
||||
Uint mipLevelCount = 1;
|
||||
while (maxDimension > 1) {
|
||||
maxDimension = std::max<Int>(maxDimension / 2, 1);
|
||||
@@ -444,13 +489,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return mipLevelCount;
|
||||
}
|
||||
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Bool depthMips) {
|
||||
return {
|
||||
std::max<Int>(baseTexelSize.x() >> static_cast<Int>(relativeLevel), 1),
|
||||
std::max<Int>(baseTexelSize.y() >> static_cast<Int>(relativeLevel), 1),
|
||||
depthMips ? std::max<Int>(baseTexelSize.z() >> static_cast<Int>(relativeLevel), 1)
|
||||
: std::max<Int>(baseTexelSize.z(), 1),
|
||||
};
|
||||
IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
|
||||
IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
|
||||
std::max<Int>(baseTexelSize.z(), 1)};
|
||||
for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
|
||||
size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
Bool EnsureGeneratedMipmapStorageAllocated(
|
||||
@@ -472,10 +517,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
|
||||
const Bool depthMips = DepthParticipatesInMipmapping(texture.GetTarget());
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, depthMips);
|
||||
const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
|
||||
const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
|
||||
for (Uint level = 1; level < requiredLevelCount; ++level) {
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, depthMips);
|
||||
const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, shrinkingAxes);
|
||||
const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
|
||||
static_cast<SizeT>(levelTexelSize.y()) *
|
||||
static_cast<SizeT>(levelTexelSize.z());
|
||||
@@ -522,44 +567,55 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampleCounts.empty() ? 0 : sampleCounts.front();
|
||||
}
|
||||
|
||||
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
|
||||
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
|
||||
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
|
||||
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
|
||||
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
|
||||
// the driver; the shadow state keeps reporting what the application asked for.
|
||||
// The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for
|
||||
// this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or
|
||||
// GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are
|
||||
// reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning
|
||||
// that an application reads GL_MAX_SAMPLES once and hands that count to every
|
||||
// glTexStorage*Multisample. That reasoning had it backwards: on Adreno and on Mali an
|
||||
// integer multisample texture is backed by ONE sample, so accepting four here did not
|
||||
// make four samples exist - ClampSamplesToBackendSupport quietly allocated one and the
|
||||
// application wrote per-sample data it could never read back. Raising INVALID_OPERATION
|
||||
// is what a real driver does, and it is what makes that silent squeeze unreachable for
|
||||
// application-visible storage.
|
||||
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
|
||||
TextureInternalFormat textureInternalFormat) {
|
||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||
return std::numeric_limits<Int>::max();
|
||||
}
|
||||
|
||||
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
|
||||
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ
|
||||
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format
|
||||
// maximum - validating against anything else is how the two answers drifted apart.
|
||||
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
|
||||
if (probedMaxSamples > 0) {
|
||||
return std::max(probedMaxSamples, advertisedMaxSamples);
|
||||
}
|
||||
|
||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
|
||||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
|
||||
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
|
||||
}
|
||||
|
||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
|
||||
const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
|
||||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
|
||||
Bool isIntegerFormat = false;
|
||||
if (!isDepthOrStencil) {
|
||||
GLenum normalizedInternalFormat =
|
||||
MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
|
||||
GLenum normalizedFormat = GL_RGBA;
|
||||
GLenum normalizedType = GL_UNSIGNED_BYTE;
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
||||
normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
|
||||
&normalizedFormat, &normalizedType);
|
||||
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
|
||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
|
||||
: dynamicParameters.MaxColorTextureSamples,
|
||||
advertisedMaxSamples);
|
||||
}
|
||||
const Int categoryMaxSamples = isDepthOrStencil ? GetAdvertisedDepthTextureMaxSamples()
|
||||
: isIntegerFormat ? GetAdvertisedIntegerMaxSamples()
|
||||
: GetAdvertisedColorTextureMaxSamples();
|
||||
|
||||
// glGetInternalformativ(GL_SAMPLES) is answered from this very list
|
||||
// (GetInternalformativ below), and GL 4.6 core 8.8 makes that query the definition of
|
||||
// the per-format maximum - so when the probe has an answer it IS the ceiling, and the
|
||||
// category limit only stands in where nothing was probed.
|
||||
//
|
||||
// This used to be max(probed, category), which made the probe dead: the walk starts
|
||||
// AT the category limit (BackendObject_DirectGLES's ProbeTextureSampleCounts) so its
|
||||
// head can never exceed it, and max() therefore always collapsed to the category
|
||||
// value. A format whose 4- and 2-sample probes fail inside a 4-sample category - a
|
||||
// float colour format under EXT_color_buffer_float is the natural instance - was
|
||||
// still accepted at 4, silently squeezed to 1 by ClampSamplesToBackendSupport, and
|
||||
// then reported as 4 by GL_TEXTURE_SAMPLES while glGetInternalformativ said 1.
|
||||
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
|
||||
return probedMaxSamples > 0 ? probedMaxSamples : categoryMaxSamples;
|
||||
}
|
||||
|
||||
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
|
||||
@@ -1130,13 +1186,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE ||
|
||||
pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
|
||||
pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) {
|
||||
// GL 4.6 core 8.10: a multisample target simply does not ACCEPT these pnames, which is
|
||||
// an INVALID_ENUM - not the INVALID_OPERATION the two BASE_LEVEL gates above report.
|
||||
// Those really are operation errors (the pname is accepted, the value is not), which is
|
||||
// presumably how the wrong class got copied down here.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Sampler state is invalid for multisample textures."));
|
||||
"Multisample textures do not accept sampler-state pnames."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The six pnames a texture object shares with a sampler object carry an enum VALUE, and an
|
||||
// unrecognised one is INVALID_ENUM. The texture path used to hand the value straight to
|
||||
// ConvertGLEnumToSamplerWrapMode / ...FilterMode and throw the Unknown away, so
|
||||
// glTexParameteri(GL_TEXTURE_WRAP_S, GL_RED) was silently accepted. Sampler objects have had
|
||||
// exactly this validator all along; calling it here rather than writing a second one is also
|
||||
// what keeps the two spellings of the same state from drifting.
|
||||
//
|
||||
// Called selectively: ValidateSamplerParam's default arm reports InvalidEnum for anything it
|
||||
// does not know, and the texture-only pnames (BASE_LEVEL, SWIZZLE_*, ...) are not in its list.
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_WRAP_S:
|
||||
case GL_TEXTURE_WRAP_T:
|
||||
case GL_TEXTURE_WRAP_R:
|
||||
case GL_TEXTURE_MIN_FILTER:
|
||||
case GL_TEXTURE_MAG_FILTER:
|
||||
case GL_TEXTURE_COMPARE_MODE:
|
||||
case GL_TEXTURE_COMPARE_FUNC:
|
||||
if (!SamplerImpl::ValidateSamplerParam(pname, static_cast<GLenum>(param))) return false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (target == TextureTarget::TextureRectangle) {
|
||||
if ((pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T) &&
|
||||
(param == GL_MIRROR_CLAMP_TO_EDGE || param == GL_MIRRORED_REPEAT || param == GL_REPEAT)) {
|
||||
@@ -1446,6 +1529,78 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The targets glTexParameter* / glGetTexParameter* accept (GL 4.6 core 8.10 and 8.11). This is a
|
||||
// SHORTER list than the one ConvertGLEnumToTextureTarget knows, and deliberately so: that
|
||||
// converter folds the six cube-map FACE targets onto TextureCubeMap because glTexImage2D and
|
||||
// glCopyTexImage2D need exactly that folding, and it maps GL_TEXTURE_BUFFER to a real target
|
||||
// because glTexBuffer needs it. Neither is a legal parameter target, so without a separate
|
||||
// predicate glTexParameteri(GL_TEXTURE_CUBE_MAP_POSITIVE_X, ...) quietly applied the parameter
|
||||
// to the bound cube map and glGetTexParameterIiv(GL_TEXTURE_BUFFER, ...) quietly answered from
|
||||
// the default texture - both GL_NO_ERROR where the spec says GL_INVALID_ENUM.
|
||||
//
|
||||
// An enum the converter does not know at all was equally silent: it produced TextureTarget::
|
||||
// Unknown, GetTextureObjectByTargetForParameter handed back the null object and every caller
|
||||
// returned without recording anything. Rejecting here closes that too, at the entry point rather
|
||||
// than at the lookup, so exactly one error is recorded.
|
||||
//
|
||||
// EXACTLY the ten targets 8.10 and 8.11 enumerate - no proxies. The spec's own asymmetry is the
|
||||
// proof: GetTexLevelParameter needs an explicit clause extending its list with PROXY_TEXTURE_1D,
|
||||
// PROXY_TEXTURE_2D and the rest, and neither TexParameter nor GetTexParameter carries one. That
|
||||
// clause is why GetTexLevelParameteriv_State/GetTexLevelParameterfv_State are deliberately NOT
|
||||
// gated by this predicate.
|
||||
//
|
||||
// Routing was not a reason to accept them: GetTextureObjectByTargetForParameter resolves a proxy
|
||||
// object only after a proxy glTexImage has run, so before that the parameter call was a silent
|
||||
// no-op and after it the parameter was applied for real - both GL_NO_ERROR, and both the same
|
||||
// silent-acceptance shape this predicate exists to close for cube faces and GL_TEXTURE_BUFFER.
|
||||
static Bool IsLegalTextureParameterTarget(GLenum target) {
|
||||
switch (target) {
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The by-NAME spelling of the same rule. glTextureParameter* has no target token, so GL 4.6 core
|
||||
// 8.10 applies the list to the texture's EFFECTIVE target instead. The four vector DSA forms
|
||||
// reach the gate above for free because they re-enter through WithTemporarilyBoundNamedTexture,
|
||||
// which synthesizes the target from the object; the two scalar forms call the per-object setter
|
||||
// directly and reached no gate at all, so glTextureParameteri on a buffer texture applied state
|
||||
// with GL_NO_ERROR while glTextureParameteriv on the same texture answered GL_INVALID_ENUM.
|
||||
static Bool ValidateNamedTextureParameterTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller) {
|
||||
if (!textureObject) return false;
|
||||
const GLenum effectiveTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
|
||||
if (IsLegalTextureParameterTarget(effectiveTarget)) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("The effective target {} does not accept texture parameters.",
|
||||
MG_Util::ConvertGLEnumToString(effectiveTarget))));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateTextureParameterTarget(GLenum target, const char* caller) {
|
||||
if (IsLegalTextureParameterTarget(target)) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("target {} does not accept texture parameters.", MG_Util::ConvertGLEnumToString(target))));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
|
||||
// (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
|
||||
// sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
|
||||
@@ -1852,6 +2007,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
// TexParameteriv/TexParameterfv are introduced in OpenGL 4.0, so do not support them for now.
|
||||
void TexParameterf_State(GLenum target, GLenum pname, GLfloat param) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
|
||||
// ======================= Converting ================================
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
@@ -1949,6 +2105,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
|
||||
// ======================= Converting ================================
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
@@ -1963,6 +2121,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Quick and dirty TexParameter*v implementation to make NeoForge happy.
|
||||
// TODO: implement the missing part
|
||||
void TexParameterfv_State(GLenum target, GLenum pname, const GLfloat* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
// ======================= Converting ================================
|
||||
@@ -1972,6 +2131,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
// The vector setters reach the border colour without passing through the per-object
|
||||
// validator the scalar ones use, so the multisample gate has to be asked for explicitly -
|
||||
// otherwise glTexParameterfv(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_BORDER_COLOR, ...)
|
||||
// is accepted while the scalar spelling of the same call is not.
|
||||
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
|
||||
SetTextureBorderColorFromFloats(textureObject, params);
|
||||
break;
|
||||
}
|
||||
@@ -1994,6 +2158,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void TexParameteriv_State(GLenum target, GLenum pname, const GLint* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
// ======================= Converting ================================
|
||||
@@ -2003,6 +2168,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// ======================= Processing ================================
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
|
||||
SetTextureBorderColorFromInts(textureObject, params);
|
||||
break;
|
||||
}
|
||||
@@ -2023,12 +2189,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void TexParameterIiv_State(GLenum target, GLenum pname, const GLint* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
|
||||
SetTextureBorderColorFromIntegerInts(textureObject, params);
|
||||
break;
|
||||
}
|
||||
@@ -2051,12 +2219,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void TexParameterIuiv_State(GLenum target, GLenum pname, const GLuint* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
switch (pname) {
|
||||
case GL_TEXTURE_BORDER_COLOR: {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
|
||||
SetTextureBorderColorFromUnsignedInts(textureObject, params);
|
||||
break;
|
||||
}
|
||||
@@ -2203,6 +2373,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
|
||||
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
|
||||
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
|
||||
@@ -2632,6 +2803,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.9 / GL_EXT_texture_buffer: the two TARGET-taking forms (glTexBuffer,
|
||||
// glTexBufferRange) accept exactly GL_TEXTURE_BUFFER, and anything else is GL_INVALID_ENUM.
|
||||
// Checked up front rather than left to fall out of "the bound object is not a buffer texture"
|
||||
// deeper in, because that path's error code depends on which entry point took it - the
|
||||
// name-taking DSA forms owe GL_INVALID_OPERATION for the same shape - and because for some
|
||||
// targets it did not reach that check at all. esextcTextureBufferErrors walks every other
|
||||
// texture target through both entry points and reads the code back each time.
|
||||
static Bool ValidateBufferTextureTarget(GLenum target, const char* caller) {
|
||||
if (target == GL_TEXTURE_BUFFER) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
std::format("target 0x{:X} is not GL_TEXTURE_BUFFER.", target)));
|
||||
return false;
|
||||
}
|
||||
|
||||
static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
|
||||
const char* caller) {
|
||||
@@ -2714,9 +2901,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
|
||||
// ===================== Error Checking ==============================
|
||||
if (!ValidateBufferTextureTarget(target, __func__)) return;
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
|
||||
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
|
||||
// The sized-format table a buffer texture accepts (GL 4.6 core table 8.15). The DSA and
|
||||
// range forms have always run this through AttachBufferToTexture; this one carried a TODO
|
||||
// instead, so glTexBuffer(GL_TEXTURE_BUFFER, GL_DEPTH_COMPONENT32F, ...) succeeded.
|
||||
if (!IsBufferTextureInternalFormat(internalformat)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
|
||||
internalformat)));
|
||||
return;
|
||||
}
|
||||
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
|
||||
// nonzero name that is not an existing buffer object is an error. This is reachable on
|
||||
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
|
||||
@@ -2740,6 +2939,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// silent no-op; the slot is never empty now that every unit/target holds its default.
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||
// Defensive: the target gate above already rejected every target but GL_TEXTURE_BUFFER,
|
||||
// whose binding slot only ever holds buffer textures.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
@@ -2767,6 +2968,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexParameterIuiv_State(GLenum target, GLenum pname, GLuint* params) {
|
||||
if (params == nullptr) return;
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
|
||||
if (pname == GL_TEXTURE_BORDER_COLOR) {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
@@ -2798,6 +3000,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void GetTexParameterIiv_State(GLenum target, GLenum pname, GLint* params) {
|
||||
if (params == nullptr) return;
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
|
||||
if (pname == GL_TEXTURE_BORDER_COLOR) {
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
@@ -2823,6 +3026,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return false;
|
||||
|
||||
// ======================= Converting ================================
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
@@ -2905,11 +3110,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
break;
|
||||
case GL_TEXTURE_BORDER_COLOR:
|
||||
if (params) {
|
||||
// glGetTexParameteriv is the exact inverse of glTexParameteriv: GL 4.6 core
|
||||
// equation 2.3 against equation 2.2 on the write side (SetTextureBorderColorFromInts).
|
||||
// A bare truncating cast turned the ~4.7e-10 that equation 2.2 makes of a small
|
||||
// integer back into 0, so the legal {0,1,2,4} round trip answered {0,0,0,0}. The raw
|
||||
// integer border colour is what glGetTexParameterIiv returns, not this.
|
||||
const auto& borderColor = textureObject->GetBorderColor();
|
||||
params[0] = static_cast<GLint>(borderColor.x());
|
||||
params[1] = static_cast<GLint>(borderColor.y());
|
||||
params[2] = static_cast<GLint>(borderColor.z());
|
||||
params[3] = static_cast<GLint>(borderColor.w());
|
||||
params[0] = MG_Util::FloatToSignedNormalizedInt32(borderColor.x());
|
||||
params[1] = MG_Util::FloatToSignedNormalizedInt32(borderColor.y());
|
||||
params[2] = MG_Util::FloatToSignedNormalizedInt32(borderColor.z());
|
||||
params[3] = MG_Util::FloatToSignedNormalizedInt32(borderColor.w());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_SWIZZLE_RGBA:
|
||||
@@ -3003,6 +3213,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetTexParameterfv_State(GLenum target, GLenum pname, GLfloat* params) {
|
||||
if (!ValidateTextureParameterTarget(target, __func__)) return;
|
||||
|
||||
// ======================= Converting ================================
|
||||
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
@@ -3296,6 +3508,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_ALPHA_SIZE:
|
||||
case GL_TEXTURE_DEPTH_SIZE:
|
||||
case GL_TEXTURE_STENCIL_SIZE:
|
||||
case GL_TEXTURE_SHARED_SIZE:
|
||||
if (params) {
|
||||
*params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname);
|
||||
}
|
||||
@@ -3468,6 +3681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
case GL_TEXTURE_ALPHA_SIZE:
|
||||
case GL_TEXTURE_DEPTH_SIZE:
|
||||
case GL_TEXTURE_STENCIL_SIZE:
|
||||
case GL_TEXTURE_SHARED_SIZE:
|
||||
if (params) {
|
||||
*params = static_cast<GLfloat>(GetTextureLevelComponentParameter(textureObject->GetFormat(), pname));
|
||||
}
|
||||
@@ -3633,9 +3847,159 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
Bool ValidateCopyTextureSubImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
|
||||
GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
|
||||
GLsizei depth, const char* caller);
|
||||
|
||||
// The destination box of a copy has to lie inside the storage the copy actually WRITES, which
|
||||
// is the requested (uploadTarget, level) pair's - not level 0's.
|
||||
//
|
||||
// This exists because the general-purpose ValidateTextureSubImageOffsets bounds everything by
|
||||
// ITextureObject::GetBaseSize(), which is hardcoded to level 0 (TextureObject::GetBaseSize ->
|
||||
// GetTexelSize(0, 0)). CopyReadFramebufferIntoMipmapRegion, meanwhile, sizes its rows and
|
||||
// slices from GetMipmapTexelSize(uploadTarget, level) and memcpys into the exact-sized
|
||||
// std::vector MipmapStorage allocated for that level, with no clamp of its own. A box that is
|
||||
// legal at level 0 and out of range at level N therefore passed validation and wrote past the
|
||||
// end of the heap allocation - e.g. a 4x4 copy at offset (4,4) into level 2 of an 8x8x4
|
||||
// GL_RGBA8 array texture ran 24 bytes past a 64-byte buffer. Every level > 0 of every
|
||||
// mipmapped texture was reachable that way, and both entry points had been no-ops before, so
|
||||
// the whole exposure arrived with their implementation.
|
||||
static Bool ValidateCopySubImageRegionAtLevel(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLint xoffset,
|
||||
GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
|
||||
GLsizei depth, const char* caller) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (mipmapTexture == nullptr) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "The destination texture has no mipmap storage."));
|
||||
return false;
|
||||
}
|
||||
const IntVec3 levelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
// A level that was never defined reports a degenerate extent. GL 4.6 core 8.6 makes
|
||||
// copying into an undefined texture image INVALID_OPERATION, and it is also what keeps the
|
||||
// writer below from indexing an empty allocation.
|
||||
if (levelSize.x() <= 0 || levelSize.y() <= 0 || levelSize.z() <= 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"The requested texture level has no storage."));
|
||||
return false;
|
||||
}
|
||||
// Signed 64-bit sums: xoffset and width are both GLint and an application may pass values
|
||||
// whose sum overflows a GLint, which would otherwise compare as negative and pass.
|
||||
const Int64 lastX = static_cast<Int64>(xoffset) + static_cast<Int64>(width);
|
||||
const Int64 lastY = static_cast<Int64>(yoffset) + static_cast<Int64>(height);
|
||||
const Int64 lastZ = static_cast<Int64>(zoffset) + static_cast<Int64>(depth);
|
||||
if (xoffset < 0 || yoffset < 0 || zoffset < 0 || lastX > levelSize.x() || lastY > levelSize.y() ||
|
||||
lastZ > levelSize.z()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("The destination region does not lie inside level {} ({}x{}x{}).", level,
|
||||
levelSize.x(), levelSize.y(), levelSize.z())));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has
|
||||
// resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between
|
||||
// the two forms: the DSA form takes a cube map and selects the face with zoffset (GL 4.6 core
|
||||
// 8.6), while the target-taking form cannot even name a cube map here - GL_TEXTURE_CUBE_MAP is
|
||||
// not in glCopyTexSubImage3D's accepted-target list, its faces go through
|
||||
// glCopyTexSubImage2D - so for it zoffset is always a layer index.
|
||||
static void CopyTextureSubImage3DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
|
||||
GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset,
|
||||
const char* caller) {
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FACE MAPPING HAS TO HAPPEN BEFORE THE BOUNDS CHECK, not after it. A cube map stores
|
||||
// its six faces as six upload targets of ONE z-slice each, so its GetBaseSize().z() is 1 -
|
||||
// and the generic offset validator, whose z bound always comes from that, rejected every
|
||||
// zoffset in 1..5 with GL_INVALID_VALUE before the mapping below could run. Five of six
|
||||
// faces were unreachable through glCopyTextureSubImage3D even though the entry point
|
||||
// documents zoffset as the face selector (GL 4.6 core 8.6). The cube bound is the FACE
|
||||
// COUNT, which the generic validator has no way to express because its `depth` parameter
|
||||
// is the copy extent; glClearTexSubImage already special-cases the same shape.
|
||||
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
GLint sliceOffset = zoffset;
|
||||
if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
|
||||
const SizeT faceCount = textureObject->GetUploadTargets().size();
|
||||
if (zoffset < 0 || static_cast<SizeT>(zoffset) >= faceCount) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"zoffset selects the cube map face and must be in [0, " + std::to_string(faceCount) + ")."));
|
||||
return;
|
||||
}
|
||||
uploadTarget = static_cast<TextureUploadTarget>(
|
||||
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
|
||||
sliceOffset = 0;
|
||||
}
|
||||
|
||||
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset,
|
||||
width, height, /*depth=*/1, caller)) {
|
||||
return;
|
||||
}
|
||||
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
|
||||
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
|
||||
width, height, caller);
|
||||
}
|
||||
|
||||
// The same for the one-dimensional pair. A 1D level is {width, 1, 1}, so the y and z arms of
|
||||
// the check above are trivially satisfied and the x arm is the whole rule - which is exactly
|
||||
// the one that overflowed: level 2 of an 8-texel GL_RGBA8 1D texture is 8 bytes, and a 4-texel
|
||||
// copy at xoffset 4 wrote 16 bytes starting 16 bytes in, entirely outside the allocation.
|
||||
static void CopyTextureSubImage1DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLint level, GLint xoffset, GLint x, GLint y, GLsizei width,
|
||||
const char* caller) {
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (width < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
|
||||
return;
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
|
||||
/*zoffset=*/0, width, /*height=*/1, /*depth=*/1, caller)) {
|
||||
return;
|
||||
}
|
||||
if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
|
||||
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
|
||||
/*zoffset=*/0, x, y, width, /*height=*/1, caller);
|
||||
}
|
||||
|
||||
void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
|
||||
GLint y, GLsizei width, GLsizei height) {
|
||||
// TODO: implement
|
||||
// GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts
|
||||
// exactly TEXTURE_3D, TEXTURE_2D_ARRAY and TEXTURE_CUBE_MAP_ARRAY. A cube map's faces are
|
||||
// two-dimensional targets of their own and go through glCopyTexSubImage2D.
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::Texture2DArray &&
|
||||
textureTarget != TextureTarget::TextureCubeMapArray) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"glCopyTexSubImage3D requires GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY or "
|
||||
"GL_TEXTURE_CUBE_MAP_ARRAY."));
|
||||
return;
|
||||
}
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
|
||||
/*allowCubeFaceFromZOffset=*/false, __func__);
|
||||
}
|
||||
|
||||
// What the three CopyTextureSubImage forms check in common (GL 4.6 core 8.6), once the caller
|
||||
@@ -4001,7 +4365,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void CopyTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
|
||||
// TODO: implement
|
||||
// The bound-texture form of glCopyTextureSubImage1D. GL 4.6 core 8.6 accepts only
|
||||
// GL_TEXTURE_1D here.
|
||||
const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
if (textureTarget != TextureTarget::Texture1D) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"glCopyTexSubImage1D requires GL_TEXTURE_1D."));
|
||||
return;
|
||||
}
|
||||
const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!textureObject) return;
|
||||
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
|
||||
}
|
||||
|
||||
Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
@@ -4457,6 +4834,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
|
||||
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
|
||||
if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
|
||||
if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
|
||||
if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
|
||||
@@ -4901,6 +5279,72 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"GetTexImage_State");
|
||||
}
|
||||
|
||||
// What this helper can and cannot answer.
|
||||
//
|
||||
// ProcessTexturePixelsDataPack performs NO format or type conversion: it sizes every texel with
|
||||
// GetInternalBytesPerPixel(the TEXTURE's internal format) and memcpys the shadow rows verbatim,
|
||||
// and it carries a standing TODO for the pixel-store parameters, so it honours only SwapBytes and
|
||||
// the bitmap LSBFirst path. Both facts are invisible from the outside, and both are dangerous:
|
||||
//
|
||||
// * a (format, type) narrower than the shadow's own texel makes the copy write MORE bytes than
|
||||
// the caller's buffer holds. glGetTexImage passes bufSize = -1 (it has no bufSize argument),
|
||||
// so the size guard below is skipped and the Memcpy runs off the end of the application's
|
||||
// allocation - reading an 8x8 GL_RGBA8 level as (GL_RED, GL_UNSIGNED_BYTE) writes 256 bytes
|
||||
// into the 64 that GL 4.6 core 8.11 says are required. A wider (format, type) is not an
|
||||
// overflow but is still wrong data.
|
||||
// * a pack state that puts padding, a row-length override or a skip offset between rows is
|
||||
// ignored outright, so the rows land at the wrong destination strides - while the GPU
|
||||
// readback path (DirectGLES StoreClientRows, and DirectVulkan through it) honours all of it.
|
||||
// Same glGetTexImage call, two different destination layouts, decided by whether the texture
|
||||
// happens to have a GPU image.
|
||||
//
|
||||
// So the copy is only correct when the client layout IS the shadow layout and the destination
|
||||
// walk is tight. That is checked here rather than assumed, and a request outside it is refused
|
||||
// with an error instead of being answered wrongly. Refusing is a real narrowing of what GL
|
||||
// promises - the spec wants the conversion performed - but the alternative on this path is a
|
||||
// heap overflow, and the conversion belongs in the pack processor rather than in another
|
||||
// open-coded copy here.
|
||||
static Bool ValidateShadowReadbackLayout(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureInputFormat textureInputFormat,
|
||||
TexturePixelDataType texturePixelDataType, GLsizei width,
|
||||
const char* caller) {
|
||||
const SizeT shadowTexelSize =
|
||||
MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
|
||||
const SizeT clientTexelSize = MG_Util::GetInputBytesPerPixel(textureInputFormat, texturePixelDataType);
|
||||
if (shadowTexelSize == 0 || clientTexelSize == 0 || shadowTexelSize != clientTexelSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("Reading this texture back needs a format/type conversion that the CPU-shadow "
|
||||
"path cannot perform: the shadow texel is {} bytes and the requested one is {}.",
|
||||
shadowTexelSize, clientTexelSize)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// A tight destination walk is the only one the pack processor produces. GL_PACK_ALIGNMENT
|
||||
// defaults to 4, so a row whose byte count is not already a multiple of it needs padding that
|
||||
// would never be written - no glPixelStorei call from the application is required to reach
|
||||
// this.
|
||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||
const SizeT alignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
|
||||
const SizeT rowBytes = static_cast<SizeT>(std::max<GLsizei>(width, 0)) * clientTexelSize;
|
||||
const Bool tightRows = (rowBytes % alignment) == 0;
|
||||
const Bool noOverrides = packParams.RowLength == 0 && packParams.ImageHeight == 0 &&
|
||||
packParams.SkipPixels == 0 && packParams.SkipRows == 0 &&
|
||||
packParams.SkipImages == 0;
|
||||
if (!tightRows || !noOverrides) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
"The CPU-shadow readback path packs rows tightly and cannot honour a pixel-store state that "
|
||||
"adds row padding, a row-length override or a skip offset."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
|
||||
@@ -4927,6 +5371,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);
|
||||
if (!ValidateShadowReadbackLayout(textureObject, textureInputFormat, texturePixelDataType, texelSize.x(),
|
||||
caller)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const void* src = textureMipmapObject->MapMipmapData(textureUploadTarget, level);
|
||||
if (!src) return;
|
||||
|
||||
@@ -5195,17 +5644,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
if (textureObject->GetTarget() == TextureTarget::TextureCubeMapArray &&
|
||||
(width != height || depth % 6 != 0)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
"Cube map array immutable storage must be square with depth multiple of 6."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
// The cube-array shape rules, shared with glTexImage3D / glCompressedTexImage3D so the
|
||||
// three cannot drift (they had: this check used to exist here and nowhere else).
|
||||
if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
|
||||
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
|
||||
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
|
||||
@@ -5785,11 +6228,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
void TextureParameteri(GLuint texture, GLenum pname, GLint param) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
|
||||
TextureParameterObject_State(textureObject, pname, param, __func__);
|
||||
}
|
||||
|
||||
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
|
||||
TextureParameterObjectf_State(textureObject, pname, param, __func__);
|
||||
}
|
||||
|
||||
@@ -6519,6 +6964,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||
// The TARGET-taking form owes GL_INVALID_ENUM for a target that is not GL_TEXTURE_BUFFER,
|
||||
// where the name-taking DSA forms below owe GL_INVALID_OPERATION for the corresponding
|
||||
// "that texture is not a buffer texture". Same shared body, different gate.
|
||||
if (!ValidateBufferTextureTarget(target, __func__)) return;
|
||||
AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
|
||||
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
|
||||
}
|
||||
@@ -6617,9 +7066,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"CopyTextureSubImage1D requires a 1D texture."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, 0, 0, width, 1, 1, __func__)) return;
|
||||
CopyReadFramebufferIntoMipmapRegion(textureObject, GetPrimaryUploadTarget(textureObject), level, xoffset,
|
||||
/*yoffset=*/0, /*zoffset=*/0, x, y, width, /*height=*/1, __func__);
|
||||
CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
|
||||
}
|
||||
|
||||
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
|
||||
@@ -6638,20 +7085,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"cube map array texture."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, zoffset, width, height, 1, __func__)) {
|
||||
return;
|
||||
}
|
||||
// A cube map addresses its faces as separate upload targets, so zoffset selects the target
|
||||
// rather than a slice within one; every other layered target keeps zoffset as the slice.
|
||||
TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
|
||||
GLint sliceOffset = zoffset;
|
||||
if (target == TextureTarget::TextureCubeMap) {
|
||||
uploadTarget = static_cast<TextureUploadTarget>(
|
||||
static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
|
||||
sliceOffset = 0;
|
||||
}
|
||||
CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
|
||||
width, height, __func__);
|
||||
CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
|
||||
/*allowCubeFaceFromZOffset=*/true, __func__);
|
||||
}
|
||||
|
||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
|
||||
|
||||
@@ -8,9 +8,24 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
|
||||
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
|
||||
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
|
||||
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
|
||||
//
|
||||
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
|
||||
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
|
||||
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
|
||||
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
|
||||
// that adds row padding / a row-length override / a skip offset, is rejected with
|
||||
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
|
||||
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
|
||||
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
|
||||
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
|
||||
// clears take the same list, so it is shared rather than written out twice.
|
||||
Bool IsBufferTextureInternalFormat(GLenum internalformat);
|
||||
|
||||
@@ -103,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller) {
|
||||
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
|
||||
return true;
|
||||
}
|
||||
if (width != height) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array levels must be square (width == height)"));
|
||||
return false;
|
||||
}
|
||||
if (depth % 6 != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Cube map array depth must be a multiple of six"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
|
||||
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
|
||||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
|
||||
|
||||
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
|
||||
Bool ValidateTextureLevelNumber(Int level);
|
||||
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
|
||||
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
|
||||
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
|
||||
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
|
||||
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
|
||||
// non-cube-array upload target answers true untouched.
|
||||
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
|
||||
const char* caller);
|
||||
Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
|
||||
Bool ValidateTextureInternalFormat(TextureInternalFormat format);
|
||||
Bool ValidateTextureBorderNumber(Int border);
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/DataTypeConverter.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
Uint GetMaxVertexAttribs() {
|
||||
constexpr Uint capacity = static_cast<Uint>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!MG_Backend::pActiveBackendObject) return capacity;
|
||||
|
||||
const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs;
|
||||
if (backendLimit <= 0) return capacity;
|
||||
return std::min(static_cast<Uint>(backendLimit), capacity);
|
||||
// Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs.
|
||||
const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr;
|
||||
const Int backendLimit =
|
||||
hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0;
|
||||
return static_cast<Uint>(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit));
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribBindings() {
|
||||
|
||||
@@ -63,8 +63,10 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
Scenarios/AdvertisedLimitsScenario.cpp
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/PrimitiveRestartScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/SampleVariablesScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
@@ -101,13 +103,18 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredAttachmentShapeScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
||||
Scenarios/StorageBufferRegrowScenario.cpp
|
||||
Scenarios/SpirvShaderBinaryScenario.cpp
|
||||
Scenarios/RelinkStageSetScenario.cpp
|
||||
Scenarios/GuiBatchScenario.cpp
|
||||
Scenarios/UnboundImageDescriptorScenario.cpp
|
||||
Scenarios/IntegerBorderColorScenario.cpp
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -56,7 +56,13 @@ namespace MGITest {
|
||||
|
||||
const std::vector<LimitBound>& BufferLimitTable() {
|
||||
static const std::vector<LimitBound> table = {
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256},
|
||||
// 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state
|
||||
// layer's indexed-binding array - the two were made to coincide when the array
|
||||
// was widened from 36, which had made the clamp in GL_Getter degenerate.
|
||||
{GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256},
|
||||
// 14 uniform blocks on each of the FIVE graphics stages. The sum used to count
|
||||
// three, and the two tessellation stages were simply missing from it.
|
||||
{GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256},
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
{GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256},
|
||||
@@ -133,6 +139,49 @@ namespace MGITest {
|
||||
<< relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = "
|
||||
<< bindings << "; a shader may declare more blocks than there are binding points to bind them to";
|
||||
}
|
||||
|
||||
// THE MIDDLE TERM, which the relation quoted above always had and this case never
|
||||
// checked. It is the one that actually broke: widening the binding-point array to 84
|
||||
// raised what every PER-STAGE count clamps to, while the combined value was a
|
||||
// five-stage sum of 70 - so a device reporting descriptor-indexing-scale uniform
|
||||
// buffers (Adreno: maxPerStageDescriptorUniformBuffers = 16777216) advertised 84
|
||||
// compute uniform blocks inside a combined limit of 70. Per-stage <= combined is
|
||||
// exactly the assertion that says so, and it costs one glGetIntegerv per row.
|
||||
struct StageAgainstCombined {
|
||||
GLenum stage;
|
||||
const char* stageName;
|
||||
GLenum combined;
|
||||
const char* combinedName;
|
||||
};
|
||||
const StageAgainstCombined stageRelations[] = {
|
||||
{GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS",
|
||||
GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_GEOMETRY_UNIFORM_BLOCKS, "GL_MAX_GEOMETRY_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_COMBINED_UNIFORM_BLOCKS,
|
||||
"GL_MAX_COMBINED_UNIFORM_BLOCKS"},
|
||||
{GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
{GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, "GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS",
|
||||
GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS"},
|
||||
};
|
||||
for (const StageAgainstCombined& relation : stageRelations) {
|
||||
GLint stage = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(relation.stage, &stage);
|
||||
glGetIntegerv(relation.combined, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.stageName;
|
||||
EXPECT_LE(stage, combined)
|
||||
<< relation.stageName << " = " << stage << " exceeds " << relation.combinedName << " = "
|
||||
<< combined << "; GL 4.6 table 23.64 orders MAX_*_BUFFER_BINDINGS >= MAX_COMBINED_*_BLOCKS >= "
|
||||
"every per-stage count, and a single-stage program may use its whole per-stage allowance";
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's
|
||||
@@ -199,6 +248,80 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// The GL 4.5 core minimums that had no case in the getter at all, or that were still
|
||||
// carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a
|
||||
// too-small value against a context advertising 4.6, and each is the FIRST call its
|
||||
// conformance case makes - so the case died before it could measure anything.
|
||||
//
|
||||
// The cull pair is deliberately absent: zero is a legal answer there (a backend with no
|
||||
// cull-distance route MUST report it), so it is checked for answerability only, below.
|
||||
TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) {
|
||||
const std::vector<LimitBound> table = {
|
||||
{GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256},
|
||||
{GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20},
|
||||
{GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20},
|
||||
// GL_MAX_VERTEX_STREAMS is deliberately absent. GL 4.5 requires 4 and MobileGL
|
||||
// answers 1, which is a KNOWN non-conformance rather than an oversight: raising
|
||||
// the number un-gates two transform-feedback CTS cases per package across
|
||||
// KHR-GL40..GL46 that then fail, because no part of the shader pipeline supports
|
||||
// layout(stream = N). See the GL_MAX_VERTEX_STREAMS case in GL_Getter.cpp. Adding
|
||||
// a row here would pin a number the implementation cannot back.
|
||||
{GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256},
|
||||
{GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20},
|
||||
{GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16},
|
||||
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024,
|
||||
1 << 20},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256},
|
||||
{GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16},
|
||||
{GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS",
|
||||
58368, 1 << 30},
|
||||
{GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS,
|
||||
"GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30},
|
||||
};
|
||||
for (const LimitBound& bound : table) {
|
||||
GLint value = -424242;
|
||||
glGetIntegerv(bound.pname, &value);
|
||||
const unsigned int error = FirstGLError();
|
||||
EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error);
|
||||
if (error != GL_NO_ERROR) continue;
|
||||
EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum "
|
||||
<< bound.minimum;
|
||||
EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling "
|
||||
<< bound.ceiling;
|
||||
}
|
||||
|
||||
// ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route,
|
||||
// so only answerability and the combined-limit ordering are checked here.
|
||||
GLint cull = -1;
|
||||
GLint clip = -1;
|
||||
GLint combined = -1;
|
||||
glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull);
|
||||
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip);
|
||||
glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error";
|
||||
EXPECT_GE(cull, 0);
|
||||
EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one";
|
||||
EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one";
|
||||
|
||||
// GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so
|
||||
// the wide query must answer it and the narrow one must saturate rather than wrap.
|
||||
GLint64 elementIndex = -1;
|
||||
glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(elementIndex, static_cast<GLint64>(4294967295LL))
|
||||
<< "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.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 - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED.
|
||||
//
|
||||
// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE
|
||||
// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level
|
||||
// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with
|
||||
// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no
|
||||
// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body
|
||||
// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and
|
||||
// all three entry points.
|
||||
//
|
||||
// The frontend understands this shape - the clear is a pure CPU-shadow write, and
|
||||
// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The
|
||||
// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a
|
||||
// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and
|
||||
// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly
|
||||
// as it found it. The conformance failures carried no <Text> at all, because nothing raised a GL
|
||||
// error: the destination was simply never written, so the test compared its own zero-initialized
|
||||
// buffer against the clear value.
|
||||
//
|
||||
// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have
|
||||
// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on
|
||||
// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would
|
||||
// return stale bytes for every render-to-texture result instead.
|
||||
//
|
||||
// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible
|
||||
// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a
|
||||
// separate change (it moves every GL-level-to-subresource translation in the backend); this
|
||||
// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able
|
||||
// to define a lone level N - is the built-in control for what the answer should be.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The conformance family's own shape: a mid-chain level of a texture that has nothing else.
|
||||
constexpr GLint kDefinedLevel = 3;
|
||||
constexpr GLsizei kLevelExtent = 8;
|
||||
|
||||
struct Texel8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
bool operator==(const Texel8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Texel8& c) {
|
||||
return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||
}
|
||||
|
||||
// The conformance test's clear value is a single repeated component; 5 is what it uses, and
|
||||
// it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one).
|
||||
constexpr Texel8 kClearValue{5, 5, 5, 5};
|
||||
constexpr Texel8 kInitialValue{200, 100, 50, 255};
|
||||
|
||||
class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_texture != 0) {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glDeleteTextures(1, &m_texture);
|
||||
m_texture = 0;
|
||||
}
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D,
|
||||
// which would define the whole chain and could not express "level 0 does not exist".
|
||||
void MakeTextureWithOnlyLevel(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> initial(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
initial.data());
|
||||
// What the conformance case does: MAX_LEVEL names the one level that exists, and
|
||||
// BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND
|
||||
// nominally the base level, the shape the backend could not express.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level;
|
||||
}
|
||||
|
||||
std::vector<Texel8> ReadLevel(GLint level) {
|
||||
std::vector<Texel8> pixels(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0});
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind";
|
||||
return pixels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, const std::vector<Texel8>& pixels, Texel8 expected) {
|
||||
std::size_t offenders = 0;
|
||||
Texel8 firstBad{};
|
||||
for (const Texel8& pixel : pixels) {
|
||||
if (pixel == expected) continue;
|
||||
if (offenders == 0) firstBad = pixel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " ("
|
||||
<< offenders << " of " << pixels.size() << " texels wrong)";
|
||||
}
|
||||
|
||||
// Level 0 defined, a GAP, then `level` defined. GL keeps the intervening levels at a zero
|
||||
// extent, so the backend's mip walk stops at the gap and the VkImage ends up with FEWER
|
||||
// mip levels than the GL level count - which is a different shape from "no image at all"
|
||||
// and is why the readback has to bound the level against the IMAGE.
|
||||
void MakeTextureWithAGapBefore(GLint level) {
|
||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
||||
glGenTextures(1, &m_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
||||
const std::vector<Texel8> base(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
base.data());
|
||||
const std::vector<Texel8> gapped(static_cast<std::size_t>(kLevelExtent) * kLevelExtent, kInitialValue);
|
||||
glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, gapped.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup with a gap before level " << level;
|
||||
}
|
||||
|
||||
GLuint m_texture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the
|
||||
// caller's buffer kept whatever it already held - which is why the conformance failures showed
|
||||
// the test's own zero-initialized memory and carried no GL error.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
// Pre-flight: the level reads back as what was uploaded. This is what makes the assertion
|
||||
// after the clear falsifiable - without it, a readback that silently wrote nothing could not
|
||||
// be told from one that wrote the right answer.
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same shape through glClearTexSubImage, which is a separate entry point in the conformance
|
||||
// family and failed on exactly the same bodies.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(kDefinedLevel);
|
||||
|
||||
glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from
|
||||
// the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally
|
||||
// would pass the two tests above and this one too - but it would also hand back stale bytes for
|
||||
// anything the GPU had written, which is why the partial-clear check below matters: the readback
|
||||
// has to see a region the backend cleared and a region it did not, in one image.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithOnlyLevel(0);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue);
|
||||
|
||||
// Clear only the left half, so the answer is neither "all initial" nor "all cleared".
|
||||
glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
&kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected";
|
||||
|
||||
const std::vector<Texel8> pixels = ReadLevel(0);
|
||||
ASSERT_EQ(pixels.size(), static_cast<std::size_t>(kLevelExtent) * kLevelExtent);
|
||||
for (int y = 0; y < kLevelExtent; ++y) {
|
||||
for (int x = 0; x < kLevelExtent; ++x) {
|
||||
const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue;
|
||||
const Texel8 actual = pixels[static_cast<std::size_t>(y) * kLevelExtent + x];
|
||||
ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")";
|
||||
}
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The adjacent shape the first fix did NOT cover: level 0 defined, a gap, then the level being
|
||||
// read. This one DOES get a VkImage - just one with fewer mip levels than GL thinks the texture
|
||||
// has - so the "no VkImage" test passes and the GL level was written straight into
|
||||
// imageSubresource.mipLevel and into a VkImageMemoryBarrier's baseMipLevel. An out-of-range
|
||||
// subresource is a promise the driver takes at face value; the glCopyImageSubData path two
|
||||
// functions away grew the same guard after it SIGSEGV'd inside the Adreno driver.
|
||||
//
|
||||
// The level being read really does hold its own data (the shadow is its only copy, since nothing
|
||||
// ever uploaded it), so the correct answer is the uploaded bytes - not a decline.
|
||||
TEST_F(ClearTexImageUndefinedLevelZeroScenario, ReadBackALevelSeparatedFromLevelZeroByAGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
MakeTextureWithAGapBefore(kDefinedLevel);
|
||||
|
||||
ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue);
|
||||
|
||||
glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected";
|
||||
|
||||
ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue);
|
||||
|
||||
// Level 0 is backed by the real image and must still read back from it, so the level bound is
|
||||
// about the level and not about the texture.
|
||||
ExpectAllTexels("level 0 after clearing level 3", ReadLevel(0), kInitialValue);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,391 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.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 - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER.
|
||||
//
|
||||
// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border
|
||||
// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and
|
||||
// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754
|
||||
// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on
|
||||
// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK.
|
||||
//
|
||||
// Two independent halves, and this scenario covers both because it goes through the frontend:
|
||||
//
|
||||
// * the STATE had no record of which entry point wrote the border colour. All three
|
||||
// representations are kept numerically in step, so the value alone cannot say whether the
|
||||
// application called glTexParameterfv or glTexParameterIiv.
|
||||
// * each backend then had exactly one border-colour call site: glTexParameterfv /
|
||||
// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on
|
||||
// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all.
|
||||
//
|
||||
// The border value is deliberately outside every predefined VkBorderColor and outside anything a
|
||||
// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black,
|
||||
// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color.
|
||||
// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit.
|
||||
//
|
||||
// Both an integer image view and an integer border colour are involved, which is the other half of
|
||||
// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour
|
||||
// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK.
|
||||
// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kOutputWidth = 8;
|
||||
constexpr int kOutputHeight = 8;
|
||||
|
||||
// The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border
|
||||
// is deliberately not derivable from the texel.
|
||||
constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44};
|
||||
constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3};
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// One channel per draw, so a failure names the component that is wrong. The coordinate is a
|
||||
// uniform rather than a literal so the same program serves the border sample and the inside
|
||||
// sample and nothing can be constant-folded differently between them.
|
||||
std::string FragmentSource(int channel) {
|
||||
static const char* kChannels[4] = {"x", "y", "z", "w"};
|
||||
return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n"
|
||||
"out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") +
|
||||
kChannels[channel] + ";\n}\n";
|
||||
}
|
||||
|
||||
class IntegerBorderColorScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
// 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory.
|
||||
const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]},
|
||||
{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}};
|
||||
glGenTextures(1, &m_sourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind";
|
||||
|
||||
// 8x8 R32I render target: an integer readback, so nothing is normalized on the way
|
||||
// out and a wrong value is reported as the number it actually was.
|
||||
glGenTextures(1, &m_outputTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_outputTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
if (m_sampler != 0) {
|
||||
glBindSampler(0, 0);
|
||||
glDeleteSamplers(1, &m_sampler);
|
||||
m_sampler = 0;
|
||||
}
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture);
|
||||
if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture);
|
||||
if (m_narrowTexture != 0) glDeleteTextures(1, &m_narrowTexture);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// Samples `coord` through the integer sampler and returns every texel the draw wrote.
|
||||
std::vector<std::int32_t> RenderChannel(int channel, float coordX, float coordY) {
|
||||
const std::string fragment = FragmentSource(channel);
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "channel " << channel << ": program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
// A clear value nothing under test can produce, so an undrawn target is not mistaken
|
||||
// for a correct one.
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
void ExpectAllTexels(const char* what, int channel, std::int32_t expected,
|
||||
const std::vector<std::int32_t>& texels) {
|
||||
if (texels.empty()) return;
|
||||
std::size_t offenders = 0;
|
||||
std::int32_t firstBad = 0;
|
||||
for (const std::int32_t texel : texels) {
|
||||
if (texel == expected) continue;
|
||||
if (offenders == 0) firstBad = texel;
|
||||
++offenders;
|
||||
}
|
||||
EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad
|
||||
<< " instead of " << expected << " (" << offenders << " of " << texels.size()
|
||||
<< " texels wrong)";
|
||||
}
|
||||
|
||||
// Every component of the border, in one place, so both the texture-object and the
|
||||
// sampler-object case assert exactly the same thing.
|
||||
void ExpectBorderIsDelivered(const char* what) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
// (-0.5, -0.5) is a full texture width outside the image on both axes, so
|
||||
// CLAMP_TO_BORDER can only answer with the border colour.
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, -0.5f, -0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind";
|
||||
ExpectAllTexels(what, channel, kBorderColor[channel], texels);
|
||||
}
|
||||
}
|
||||
|
||||
// A narrow-format source built on demand, for the clamp cases. Returns the texture, which
|
||||
// the caller owns until TearDown deletes it through m_narrowTexture.
|
||||
void MakeNarrowSource(GLenum internalFormat, GLenum clientFormat, const void* texels,
|
||||
const GLint* border, bool borderIsUnsigned) {
|
||||
glGenTextures(1, &m_narrowTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, 2, 2);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, clientFormat,
|
||||
internalFormat == GL_R8UI ? GL_UNSIGNED_BYTE : GL_BYTE, texels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
if (borderIsUnsigned) {
|
||||
const GLuint asUnsigned[4] = {static_cast<GLuint>(border[0]), static_cast<GLuint>(border[1]),
|
||||
static_cast<GLuint>(border[2]), static_cast<GLuint>(border[3])};
|
||||
glTexParameterIuiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, asUnsigned);
|
||||
} else {
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "narrow source setup left a GL error behind";
|
||||
}
|
||||
|
||||
// The narrow sources are single-channel, so only component 0 carries anything, and the
|
||||
// sampler declaration has to match the format's signedness.
|
||||
std::vector<std::int32_t> RenderNarrowBorder(bool isUnsignedSampler) {
|
||||
const std::string fragment =
|
||||
std::string("#version 330 core\n\nuniform ") + (isUnsignedSampler ? "usampler2D" : "isampler2D") +
|
||||
" smp;\nuniform vec2 uCoord;\n\nout int out_color;\n\nvoid main()\n{\n"
|
||||
" out_color = int(texture(smp, uCoord).x);\n}\n";
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error);
|
||||
if (program == 0) {
|
||||
ADD_FAILURE() << "narrow-border program did not build: " << error;
|
||||
return {};
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glViewport(0, 0, kOutputWidth, kOutputHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
const GLint clearValue[4] = {-559038737, 0, 0, 0};
|
||||
glClearBufferiv(GL_COLOR, 0, clearValue);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_narrowTexture);
|
||||
glUniform1i(glGetUniformLocation(program, "smp"), 0);
|
||||
glUniform2f(glGetUniformLocation(program, "uCoord"), -0.5f, -0.5f);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
std::vector<std::int32_t> texels(static_cast<std::size_t>(kOutputWidth) * kOutputHeight, 0);
|
||||
glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data());
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(program);
|
||||
return texels;
|
||||
}
|
||||
|
||||
GLuint m_sourceTexture = 0;
|
||||
GLuint m_outputTexture = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_sampler = 0;
|
||||
GLuint m_narrowTexture = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE
|
||||
// sample has to fetch the texture's own texel. If this fails the sampler, the shader or the
|
||||
// integer readback is broken and nothing about the border colour has been measured.
|
||||
TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const std::vector<std::int32_t> texels = RenderChannel(channel, 0.5f, 0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind";
|
||||
ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels);
|
||||
}
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend
|
||||
// already accepted and then flattened into the same FloatVec4 every other spelling wrote.
|
||||
TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glTexParameterIiv");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The regression, sampler-object spelling - which is the one the conformance cases actually use,
|
||||
// and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the
|
||||
// sampler cache key on DirectVulkan, where a border colour that is not part of the key would
|
||||
// alias two samplers that differ only in it).
|
||||
TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glGenSamplers(1, &m_sampler);
|
||||
ASSERT_NE(m_sampler, 0u);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected";
|
||||
|
||||
// The texture object carries a DIFFERENT border colour, so a pass here cannot come from the
|
||||
// texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state
|
||||
// wins over the texture's for every sampling parameter.
|
||||
const std::int32_t decoyBorder[4] = {0, 0, 0, 0};
|
||||
glBindTexture(GL_TEXTURE_2D, m_sourceTexture);
|
||||
glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder);
|
||||
glBindSampler(0, m_sampler);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected";
|
||||
|
||||
ExpectBorderIsDelivered("glSamplerParameterIiv");
|
||||
glBindSampler(0, 0);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "For floating-point and integer formats, border values are clamped to the
|
||||
// representable range of the format." A border of 300 on a GL_R8I texture is 127, not 300 - and
|
||||
// VK_BORDER_COLOR_INT_CUSTOM_EXT delivers whatever it is handed, with format VK_FORMAT_UNDEFINED
|
||||
// there is nothing for the driver to clamp against, so the clamp has to happen before the value
|
||||
// leaves MobileGL. DirectGLES gets it right for free (the ES driver knows the texture format),
|
||||
// which is what makes this a cross-backend divergence and not only a spec one.
|
||||
TEST_F(IntegerBorderColorScenario, ASignedIntegerBorderIsClampedToTheFormatsRepresentableRange) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::int8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {300, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8I, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/false);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8I border 300", 0, 127, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The reciprocal half, and the one that decides how the two integer forms relate: -1 written
|
||||
// through glTexParameterIiv against an UNSIGNED format. GL 4.6 core 8.10 stores an "I"-form
|
||||
// border unmodified with an integer internal data type and defines no sign conversion between
|
||||
// the two integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness: 0xFFFFFFFF, clamped to the format's maximum of 255.
|
||||
//
|
||||
// That is the DRIVER's answer, established by running this case rather than by reading the spec:
|
||||
// clamping to 0 is an equally defensible reading of the same paragraph, and DirectVulkan can be
|
||||
// made to produce either - but DirectGLES forwards the value to the ES driver verbatim and cannot
|
||||
// deviate, so choosing 0 would mean the same program sampling 0 on Magma and 255 on Espryt. The
|
||||
// whole point of carrying the border colour's form is to stop that class of divergence, so the
|
||||
// backends agree on the driver's answer.
|
||||
//
|
||||
// The clamp itself is still doing the work: without it the value reaches the driver as
|
||||
// 0xFFFFFFFF against a format whose maximum is 255, with format VK_FORMAT_UNDEFINED and so
|
||||
// nothing for the driver to clamp against.
|
||||
TEST_F(IntegerBorderColorScenario, ANegativeBorderOnAnUnsignedFormatClampsToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {-1, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/false);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border -1", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The same clamp from the unambiguous side: a value written through the UNSIGNED form that is
|
||||
// simply too large for the format. No sign reinterpretation is involved, so both backends and
|
||||
// the spec agree that 5000 on a GL_R8UI texture is 255.
|
||||
TEST_F(IntegerBorderColorScenario, AnOversizedUnsignedBorderIsClampedToTheFormatsMaximum) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
const std::uint8_t texels[4] = {1, 1, 1, 1};
|
||||
const GLint border[4] = {5000, 0, 0, 1};
|
||||
MakeNarrowSource(GL_R8UI, GL_RED_INTEGER, texels, border, /*borderIsUnsigned=*/true);
|
||||
|
||||
const std::vector<std::int32_t> sampled = RenderNarrowBorder(/*isUnsignedSampler=*/true);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the clamped-border draw left a GL error behind";
|
||||
ExpectAllTexels("R8UI border 5000", 0, 255, sampled);
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,962 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.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 - THE ATTACHMENT SHAPES A LAYERED FRAMEBUFFER CAN TAKE, AND THE ONE VIEW TYPE
|
||||
// VULKAN ACCEPTS FOR ALL OF THEM.
|
||||
//
|
||||
// glFramebufferTexture on a GL_TEXTURE_3D or a GL_TEXTURE_CUBE_MAP_ARRAY makes a LAYERED
|
||||
// framebuffer: one attachment that covers every slice / layer-face, addressed by a geometry
|
||||
// shader writing gl_Layer. Vulkan has exactly one legal spelling for that
|
||||
// (VUID-VkFramebufferCreateInfo-flags-04113: an attachment view must be VK_IMAGE_VIEW_TYPE_2D
|
||||
// or _2D_ARRAY), and DirectVulkan used to hand vkCreateFramebuffer the IMAGE's own view type
|
||||
// instead:
|
||||
//
|
||||
// * GL_TEXTURE_3D -> VK_IMAGE_VIEW_TYPE_3D. A 3D image has arrayLayers == 1 and keeps its
|
||||
// layers on z, so the layer-span guard measured [0, depth) against 1, refused, and returned
|
||||
// VK_NULL_HANDLE - which then went into pAttachments as a null handle.
|
||||
// * GL_TEXTURE_CUBE_MAP_ARRAY -> VK_IMAGE_VIEW_TYPE_CUBE_ARRAY. A perfectly valid view, of a
|
||||
// type no framebuffer may take. The driver dereferenced or rejected it inside
|
||||
// vkCreateFramebuffer.
|
||||
//
|
||||
// Both exits were guarded only by MOBILEGL_ASSERT, which an INFO build (the production and CTS
|
||||
// default) compiles to nothing - so both were process kills, not wrong pixels: 51 lost QPA
|
||||
// records over 7 conformance bodies, one runner restart each.
|
||||
//
|
||||
// The same function is what routes a NON-layered slice of a 3D texture
|
||||
// (glFramebufferTextureLayer), and it had the mirror-image hole: it asked for a 3D view there
|
||||
// too, so the per-slice branch that exists for exactly this case was unreachable and every
|
||||
// slice above z = 0 came back VK_NULL_HANDLE.
|
||||
//
|
||||
// The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
|
||||
// its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
|
||||
// ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
||||
// not merely survival: what a layer receives is a function of its own index, so an attachment that
|
||||
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
|
||||
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
|
||||
// never landed here" reads differently from "the wrong layer landed here".
|
||||
//
|
||||
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
|
||||
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
|
||||
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
|
||||
// sat beside a non-layered depth/stencil one and BOTH backends silently lost the draw. That is
|
||||
// the shape texture_cube_map_array.stencil_attachments_*_layered and
|
||||
// geometry_shader.layered_framebuffer.stencil_support are built on, and it is why they fail on
|
||||
// Espryt as well as crashing on Magma. Case (5) is what found it.
|
||||
//
|
||||
// DirectGLES is the control: it hands the same GL calls to the driver, so a red on both backends
|
||||
// means the scenario is wrong - or the defect is in the shared front end, as it was above - and a
|
||||
// red on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 4;
|
||||
// Four z slices: enough that "only slice 0 was written" and "the whole thing was written"
|
||||
// are different answers, and small enough that the geometry shader stays well inside
|
||||
// GL_MAX_GEOMETRY_OUTPUT_VERTICES.
|
||||
constexpr int k3DSlices = 4;
|
||||
// Two cubes. One cube would let "attached a single cube" pass; twelve layer-faces would
|
||||
// not.
|
||||
constexpr int kCubeLayerFaces = 12;
|
||||
// The slice a non-layered 3D attachment names. Not 0: slice 0 is the one address that is
|
||||
// right whether or not the slice is resolved at all.
|
||||
constexpr int kSubjectSlice = 2;
|
||||
|
||||
// Layers of the 1D array whose clear the last case checks. Its layer count lives in the
|
||||
// state-side HEIGHT, not in z, which is the whole reason it is here.
|
||||
constexpr int kOneDArrayLayers = 4;
|
||||
|
||||
// A colour no pass paints, uploaded before every draw. A layer that reads it back was
|
||||
// never rendered to.
|
||||
constexpr GLubyte kPoison = 0xAB;
|
||||
|
||||
// The glClear colour the two materialise cases use. Chosen as exact 8-bit values and fed
|
||||
// to glClearColor as n/255, so the round trip through a UNORM8 target is lossless and a
|
||||
// mismatch means a real miss rather than rounding.
|
||||
constexpr Rgba8 kClearColor{17, 68, 187, 255};
|
||||
|
||||
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
|
||||
// says which layer it came from) and b names the PASS (so "the second draw was not
|
||||
// rejected" is distinguishable from "the first draw never happened").
|
||||
Rgba8 ExpectedColor(int layer, int pass) {
|
||||
return {static_cast<GLubyte>(10 + layer * 20), static_cast<GLubyte>(200 - layer * 10),
|
||||
static_cast<GLubyte>(3 + pass * 60), 255};
|
||||
}
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " +
|
||||
std::to_string(color.b) + ", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// A full-viewport triangle built from gl_VertexID, so nothing here needs a vertex buffer
|
||||
// and the draw cannot fail for a reason that has nothing to do with the attachment.
|
||||
// u_depth is the NDC z the whole primitive sits at - the depth/stencil case needs two
|
||||
// different ones.
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
uniform float u_depth;
|
||||
void main()
|
||||
{
|
||||
vec2 corner = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);
|
||||
gl_Position = vec4(corner, u_depth, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The layer count is baked in as a literal rather than passed as a uniform: a
|
||||
// non-constant loop bound in a geometry shader is legal but is one more thing the
|
||||
// ESSL transpile could get wrong, and this scenario is not about that.
|
||||
std::string MakeGeometrySource(int layerCount) {
|
||||
return "#version 420 core\n"
|
||||
"layout(triangles) in;\n"
|
||||
"layout(triangle_strip, max_vertices = " +
|
||||
std::to_string(layerCount * 3) +
|
||||
") out;\n"
|
||||
"flat out int v_layer;\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
" for (int layer = 0; layer < " +
|
||||
std::to_string(layerCount) +
|
||||
"; ++layer) {\n"
|
||||
" for (int i = 0; i < 3; ++i) {\n"
|
||||
" gl_Layer = layer;\n"
|
||||
" v_layer = layer;\n"
|
||||
" gl_Position = gl_in[i].gl_Position;\n"
|
||||
" EmitVertex();\n"
|
||||
" }\n"
|
||||
" EndPrimitive();\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
}
|
||||
|
||||
const char* const kLayeredFragmentSource = R"(#version 420 core
|
||||
flat in int v_layer;
|
||||
uniform int u_pass;
|
||||
out vec4 o_color;
|
||||
void main()
|
||||
{
|
||||
o_color = vec4(float(10 + v_layer * 20) / 255.0,
|
||||
float(200 - v_layer * 10) / 255.0,
|
||||
float(3 + u_pass * 60) / 255.0,
|
||||
1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The two clear cases do not draw into the layered attachment at all - they SAMPLE it, so
|
||||
// the queued clear is consumed by MaterializePendingClearForTexture rather than by a render
|
||||
// pass's LOAD_OP_CLEAR. What the sample returns is irrelevant; being sampled is the point.
|
||||
const char* const kCubeSampleFragmentSource = R"(#version 420 core
|
||||
uniform samplerCube u_source;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texture(u_source, vec3(1.0, 0.0, 0.0)); }
|
||||
)";
|
||||
|
||||
const char* const kOneDArraySampleFragmentSource = R"(#version 420 core
|
||||
uniform sampler1DArray u_source;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texture(u_source, vec2(0.5, 0.0)); }
|
||||
)";
|
||||
|
||||
// The non-layered case has no geometry stage at all - the slice comes from the
|
||||
// attachment, not from gl_Layer - so it names its layer through a uniform.
|
||||
const char* const kFlatFragmentSource = R"(#version 420 core
|
||||
uniform int u_layer;
|
||||
uniform int u_pass;
|
||||
out vec4 o_color;
|
||||
void main()
|
||||
{
|
||||
o_color = vec4(float(10 + u_layer * 20) / 255.0,
|
||||
float(200 - u_layer * 10) / 255.0,
|
||||
float(3 + u_pass * 60) / 255.0,
|
||||
1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
class LayeredAttachmentShapeScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
for (const GLuint fbo : m_fbos) glDeleteFramebuffers(1, &fbo);
|
||||
m_fbos.clear();
|
||||
for (const GLuint texture : m_textures) glDeleteTextures(1, &texture);
|
||||
m_textures.clear();
|
||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
||||
m_programs.clear();
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
m_vao = 0;
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
// 0 on a DirectGLES driver without GL_EXT_geometry_shader and on a DirectVulkan
|
||||
// device without the geometryShader feature. The same probe GeometryDrawModeScenario
|
||||
// and IoBlockNameCollisionScenario use.
|
||||
static bool BackendHostsGeometry() {
|
||||
GLint maxGeometryOutputVertices = 0;
|
||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||
DrainErrors();
|
||||
return maxGeometryOutputVertices >= kCubeLayerFaces * 3;
|
||||
}
|
||||
|
||||
static std::string InfoLog(GLuint object, bool isShader) {
|
||||
GLint length = 0;
|
||||
if (isShader) {
|
||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
} else {
|
||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
||||
}
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
if (isShader) {
|
||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
} else {
|
||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
// geometrySource may be null, which builds the no-geometry-stage program the
|
||||
// non-layered case uses.
|
||||
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||
std::vector<GLuint> shaders;
|
||||
const auto compile = [&](GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
shaders.push_back(shader);
|
||||
if (compiled == GL_FALSE) {
|
||||
ADD_FAILURE() << "stage 0x" << std::hex << stage << std::dec
|
||||
<< " did not compile: " << InfoLog(shader, true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
bool ok = compile(GL_VERTEX_SHADER, kVertexSource);
|
||||
if (ok && geometrySource != nullptr) ok = compile(GL_GEOMETRY_SHADER, geometrySource);
|
||||
if (ok) ok = compile(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (!ok) {
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
||||
glLinkProgram(program);
|
||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_FALSE) {
|
||||
ADD_FAILURE() << "the program did not link: " << InfoLog(program, false);
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
m_programs.push_back(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint TrackTexture() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint TrackFramebuffer() {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// An RGBA8 3D texture, every texel poisoned.
|
||||
GLuint MakePoisoned3DColor() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_3D, texture);
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kExtent, kExtent, k3DSlices);
|
||||
const std::vector<GLubyte> seed(
|
||||
static_cast<std::size_t>(kExtent) * kExtent * k3DSlices * 4, kPoison);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kExtent, kExtent, k3DSlices, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, seed.data());
|
||||
glBindTexture(GL_TEXTURE_3D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// An RGBA8 cube-map array of kCubeLayerFaces layer-faces, every texel poisoned.
|
||||
GLuint MakePoisonedCubeArrayColor() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, kExtent, kExtent, kCubeLayerFaces);
|
||||
const std::vector<GLubyte> seed(
|
||||
static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces * 4, kPoison);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent, kCubeLayerFaces,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A plain RGBA8 CUBE MAP (not an array), every face poisoned. This is the shape whose
|
||||
// layered attachment records the +X face as its representative upload target, so its
|
||||
// level size reads z = 1 - the reason a shared layer-count helper is needed at all.
|
||||
GLuint MakePoisonedCubeMap() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
|
||||
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kExtent * 4, kPoison);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
|
||||
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
|
||||
// gives 1 however many layers there are.
|
||||
GLuint MakePoisoned1DArray() {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kOneDArrayLayers * 4, kPoison);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kExtent, kOneDArrayLayers, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, seed.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A scratch 2D colour target for the sampling draw. It exists only so the draw has
|
||||
// somewhere to go that is NOT the layered attachment under test - a draw into that
|
||||
// would open a render pass and consume the pending clear through LOAD_OP_CLEAR, which
|
||||
// is the other consumer and the one that was already right.
|
||||
GLuint MakeScratchColorFbo() {
|
||||
const GLuint scratch = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_2D, scratch);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kExtent, kExtent);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scratch, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// One draw that SAMPLES `texture`, into `intoFbo`. This is what drags the queued clear
|
||||
// through MaterializePendingClearForTexture (VulkanRenderer's sampled-texture
|
||||
// pre-pass), which is the consumer that used to write the clear key's layerCount
|
||||
// straight into a VkImageSubresourceRange.
|
||||
void DrawSampling(GLuint program, GLuint intoFbo, GLenum textureTarget, GLuint texture) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, intoFbo);
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(textureTarget, texture);
|
||||
glUseProgram(program);
|
||||
const GLint sourceLocation = glGetUniformLocation(program, "u_source");
|
||||
ASSERT_GE(sourceLocation, 0) << "u_source was not reflected";
|
||||
glUniform1i(sourceLocation, 0);
|
||||
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
||||
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
|
||||
glUniform1f(depthLocation, 0.0f);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindTexture(textureTarget, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
// Every texel of `texels` is the clear colour. +/-1 per channel, which no rounding can
|
||||
// exceed and which cannot be confused with the poison (0xAB) it replaced.
|
||||
void ExpectAllCleared(const std::vector<Rgba8>& texels, int perTexelStride, const char* what) {
|
||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
||||
const Rgba8& actual = texels[i];
|
||||
const bool ok = std::abs(static_cast<int>(actual.r) - kClearColor.r) <= 1 &&
|
||||
std::abs(static_cast<int>(actual.g) - kClearColor.g) <= 1 &&
|
||||
std::abs(static_cast<int>(actual.b) - kClearColor.b) <= 1;
|
||||
if (ok) continue;
|
||||
ADD_FAILURE() << what << ": unit " << (static_cast<int>(i) / perTexelStride) << " texel "
|
||||
<< (static_cast<int>(i) % perTexelStride) << " is " << Describe(actual)
|
||||
<< ", expected " << Describe(kClearColor)
|
||||
<< (actual.r == kPoison && actual.g == kPoison
|
||||
? " - the poison, so the clear never reached this one"
|
||||
: "");
|
||||
// One message per unit is enough to say what happened.
|
||||
i = (static_cast<std::size_t>(i) / perTexelStride + 1) * perTexelStride - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// A depth (or packed depth-stencil) cube-map array of the same shape. No upload: a
|
||||
// depth array is filled by clearing through an attachment, which is the state the
|
||||
// gating cases start from anyway.
|
||||
GLuint MakeCubeArrayDepth(GLenum internalFormat) {
|
||||
const GLuint texture = TrackTexture();
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent, kCubeLayerFaces);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// glGetTexImage rather than a per-layer glReadPixels: a cube-map array has no
|
||||
// per-layer attachment on every backend, and glGetTexImage is the readback both of
|
||||
// them answer for whole-level layered targets (LayeredTextureReadbackScenario pins
|
||||
// that contract). It is a real GPU readback on DirectVulkan - the texture manager
|
||||
// copies the image into a staging buffer - so a stale CPU shadow cannot pass it.
|
||||
std::vector<Rgba8> ReadLevel(GLenum target, GLuint texture, int layers) {
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent * layers, Rgba8{});
|
||||
glBindTexture(target, texture);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glGetTexImage(target, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(target, 0);
|
||||
return texels;
|
||||
}
|
||||
|
||||
// Every texel of every layer must be that layer's expected colour. Reported per layer
|
||||
// so a failure names which one, and the poison is called out by name.
|
||||
void ExpectEveryLayer(const std::vector<Rgba8>& texels, int layers, int pass, const char* what) {
|
||||
for (int layer = 0; layer < layers; ++layer) {
|
||||
const Rgba8 expected = ExpectedColor(layer, pass);
|
||||
for (int y = 0; y < kExtent; ++y) {
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
const std::size_t index =
|
||||
(static_cast<std::size_t>(layer) * kExtent + y) * kExtent + x;
|
||||
const Rgba8 actual = texels[index];
|
||||
if (actual == expected) continue;
|
||||
ADD_FAILURE()
|
||||
<< what << ": layer " << layer << " texel (" << x << ", " << y << ") is "
|
||||
<< Describe(actual) << ", expected " << Describe(expected)
|
||||
<< (actual.r == kPoison && actual.g == kPoison
|
||||
? " - the poison, so nothing was ever rendered into this layer"
|
||||
: "");
|
||||
// One message per layer is enough to say what happened.
|
||||
y = kExtent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
||||
}
|
||||
|
||||
// One layered pass over the whole attachment.
|
||||
void DrawLayered(GLuint program, int pass, float depth) {
|
||||
glUseProgram(program);
|
||||
const GLint passLocation = glGetUniformLocation(program, "u_pass");
|
||||
ASSERT_GE(passLocation, 0) << "u_pass was not reflected";
|
||||
glUniform1i(passLocation, pass);
|
||||
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
||||
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
|
||||
glUniform1f(depthLocation, depth);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0;
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
std::vector<GLuint> m_programs;
|
||||
};
|
||||
|
||||
// (1) A LAYERED GL_TEXTURE_3D colour attachment. Pre-fix this is the null VkImageView:
|
||||
// the attachment asked for a 3D view, whose [0, 4) layer span was measured against the
|
||||
// image's arrayLayers == 1 and refused, and VK_NULL_HANDLE went to vkCreateFramebuffer.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredThreeDColorAttachmentReachesEverySlice) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
||||
|
||||
const std::string geometrySource = MakeGeometrySource(k3DSlices);
|
||||
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint color = MakePoisoned3DColor();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
|
||||
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 3D texture layered failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
|
||||
ExpectEveryLayer(texels, k3DSlices, /*pass=*/0, "layered GL_TEXTURE_3D colour attachment");
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (2) The same texture attached ONE SLICE at a time, which is the other half of the same
|
||||
// view-type decision. Pre-fix a non-layered 3D attachment also asked for a 3D view, so
|
||||
// the per-slice branch never ran and slice 2 resolved to VK_NULL_HANDLE. Needs no
|
||||
// geometry stage - the slice comes from the attachment.
|
||||
TEST_F(LayeredAttachmentShapeScenario, NonLayeredThreeDSliceAttachmentWritesOnlyThatSlice) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildProgram(nullptr, kFlatFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint color = MakePoisoned3DColor();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
|
||||
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0, kSubjectSlice);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching slice " << kSubjectSlice << " failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glUseProgram(program);
|
||||
const GLint layerLocation = glGetUniformLocation(program, "u_layer");
|
||||
const GLint passLocation = glGetUniformLocation(program, "u_pass");
|
||||
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
||||
ASSERT_GE(layerLocation, 0);
|
||||
ASSERT_GE(passLocation, 0);
|
||||
ASSERT_GE(depthLocation, 0);
|
||||
glUniform1i(layerLocation, kSubjectSlice);
|
||||
glUniform1i(passLocation, 0);
|
||||
glUniform1f(depthLocation, 0.0f);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the per-slice draw errored";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
|
||||
|
||||
const Rgba8 expected = ExpectedColor(kSubjectSlice, 0);
|
||||
const Rgba8 poison{kPoison, kPoison, kPoison, kPoison};
|
||||
for (int slice = 0; slice < k3DSlices; ++slice) {
|
||||
const Rgba8& target = (slice == kSubjectSlice) ? expected : poison;
|
||||
for (int y = 0; y < kExtent; ++y) {
|
||||
for (int x = 0; x < kExtent; ++x) {
|
||||
const std::size_t index =
|
||||
(static_cast<std::size_t>(slice) * kExtent + y) * kExtent + x;
|
||||
const Rgba8 actual = texels[index];
|
||||
if (actual == target) continue;
|
||||
ADD_FAILURE() << "slice " << slice << " texel (" << x << ", " << y << ") is "
|
||||
<< Describe(actual) << ", expected " << Describe(target)
|
||||
<< (slice == kSubjectSlice
|
||||
? " - the attached slice was not the one written"
|
||||
: " - a slice the attachment did not name was written");
|
||||
y = kExtent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (3) A LAYERED GL_TEXTURE_CUBE_MAP_ARRAY colour attachment. Pre-fix this is the other
|
||||
// exit: a valid CUBE_ARRAY view of a type no framebuffer may take, handed straight to
|
||||
// vkCreateFramebuffer.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayColorAttachmentReachesEveryLayerFace) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
||||
|
||||
const GLuint color = MakePoisonedCubeArrayColor();
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
||||
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube-map array layered failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
||||
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
||||
"layered GL_TEXTURE_CUBE_MAP_ARRAY colour attachment");
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (4) A layered cube-map-array DEPTH attachment, proved to have covered every layer-face:
|
||||
//
|
||||
// pass 0 paints at z = 0 against a depth buffer cleared to 1;
|
||||
// pass 1 paints at z = +0.5, which GL_LESS must reject.
|
||||
//
|
||||
// A layer that reads back pass 1's colour is a layer the depth attachment never covered -
|
||||
// which is exactly what attaching one layer-face of it, or none, looks like. Runs on both
|
||||
// backends: this is the cross-backend control for the packed case below.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthAttachmentGatesEveryLayerFace) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
||||
|
||||
const GLuint color = MakePoisonedCubeArrayColor();
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const GLuint depth = MakeCubeArrayDepth(GL_DEPTH_COMPONENT24);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no depth GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
||||
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth pair failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth attachment errored";
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
||||
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f); // farther: GL_LESS must reject it
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the two layered draws errored";
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
||||
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
||||
"layered cube-map-array depth attachment (pass 1's colour on a layer means the "
|
||||
"depth test did not cover it)");
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (5) The PACKED depth-stencil shape the conformance suite crashes on:
|
||||
// texture_cube_map_array.stencil_attachments_*_layered attaches a cube-map array as COLOR0
|
||||
// AND the same-shaped GL_DEPTH24_STENCIL8 array as GL_DEPTH_STENCIL_ATTACHMENT, both
|
||||
// layered. Both aspects are proved to have covered every layer-face:
|
||||
//
|
||||
// pass 0 paints at z = 0 with the stencil op writing 1;
|
||||
// pass 1 paints at z = +0.5, which the depth test must reject;
|
||||
// pass 2 paints with the depth test off but a stencil func of EQUAL 0, which the
|
||||
// stencil written by pass 0 must reject.
|
||||
//
|
||||
// The probe in front of the gating is where this scenario earned its keep. The attachment
|
||||
// point ITSELF was broken: glFramebufferTexture(GL_DEPTH_STENCIL_ATTACHMENT) is a
|
||||
// shorthand that the front end splits into a depth and a stencil attachment, and the split
|
||||
// dropped the call's `layered` flag (GL_Framebuffer.cpp,
|
||||
// AttachFramebufferTextureWithUploadTarget). A layered colour attachment therefore sat
|
||||
// beside a NON-layered depth/stencil one, and both backends lost the draw entirely - with
|
||||
// no GL error and glCheckFramebufferStatus answering COMPLETE. DirectVulkan built the
|
||||
// depth/stencil view with layerCount 1 under a framebuffer declaring 12 layers
|
||||
// (VUID-VkFramebufferCreateInfo-flags-04535, which the validation layers report on this
|
||||
// exact case); DirectGLES attached one layer of it beside a layered colour target, which
|
||||
// the driver answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS. Case (4) above is what
|
||||
// isolates it to the attachment point: the same cube-map array on GL_DEPTH_ATTACHMENT
|
||||
// rendered and gated correctly throughout.
|
||||
//
|
||||
// So the probe stays, as an assertion rather than as scaffolding: it turns that regression
|
||||
// back into ONE message about the shape instead of twelve about individual layers.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthStencilAttachmentGatesEveryLayerFace) {
|
||||
if (!Ready()) return;
|
||||
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
||||
|
||||
const GLuint color = MakePoisonedCubeArrayColor();
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const GLuint depthStencil = MakeCubeArrayDepth(GL_DEPTH24_STENCIL8);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no depth-stencil GL_TEXTURE_CUBE_MAP_ARRAY on this backend: "
|
||||
<< GLErrorName(error);
|
||||
}
|
||||
|
||||
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
||||
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
|
||||
// The probe: its own colour attachment (so the subject texture keeps its poison), the
|
||||
// same depth-stencil attachment, and both tests off - so every layer-face must come
|
||||
// back painted, whatever the gating below then decides.
|
||||
{
|
||||
const GLuint probeColor = MakePoisonedCubeArrayColor();
|
||||
const GLuint probeFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, probeFbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, probeColor, 0);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
DrawLayered(program, /*pass=*/3, /*depth=*/0.0f);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> probeTexels =
|
||||
ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, probeColor, kCubeLayerFaces);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the probe draw or readback errored";
|
||||
ExpectEveryLayer(probeTexels, kCubeLayerFaces, /*pass=*/3,
|
||||
"a layered draw with the depth and stencil tests DISABLED, into a colour + "
|
||||
"GL_DEPTH_STENCIL_ATTACHMENT cube-map-array pair (all poison means the "
|
||||
"attachment pair lost the draw outright, which is what a non-layered "
|
||||
"depth/stencil attachment beside a layered colour one looks like)");
|
||||
// The gating assertions below can only add noise once the shape itself is broken.
|
||||
if (::testing::Test::HasNonfatalFailure()) return;
|
||||
}
|
||||
|
||||
const GLuint fbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glClearDepth(1.0);
|
||||
glClearStencil(0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth-stencil attachment errored";
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
||||
|
||||
// Farther than pass 0, so GL_LESS must reject it on every layer.
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f);
|
||||
|
||||
// Depth out of the way; only the stencil pass 0 wrote can reject this one.
|
||||
glDepthFunc(GL_ALWAYS);
|
||||
glStencilFunc(GL_EQUAL, 0, 0xFFu);
|
||||
DrawLayered(program, /*pass=*/2, /*depth=*/-0.5f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the three layered draws errored";
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
||||
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
||||
"layered cube-map-array depth-stencil attachment (a later pass's colour means "
|
||||
"the depth or stencil test did not cover that layer)");
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// (6) and (7) leave the render pass alone entirely and pin the OTHER consumer of a layered
|
||||
// attachment's layer count.
|
||||
//
|
||||
// A glClear on a texture-backed FBO with the scissor test off is not executed on the spot:
|
||||
// it is queued (VkClearManager), and then exactly one of two things consumes it - the next
|
||||
// render pass's LOAD_OP_CLEAR over the attachment view, or MaterializePendingClearForTexture
|
||||
// if the texture is used outside a pass first (sampled, blitted, copied, read back). The
|
||||
// second path writes the queued key's layerCount straight into a VkImageSubresourceRange
|
||||
// and then POPS the entry, so whatever it misses is lost for good - the render pass never
|
||||
// gets a second chance at it.
|
||||
//
|
||||
// Both consumers must therefore agree about how many layers a layered attachment spans, and
|
||||
// they are now literally the same function (ResolveAttachmentLayerCount, VkTextureManager.h).
|
||||
// These two cases are the shapes where a raw `size.z()` and the real answer differ, and
|
||||
// neither is reachable through the cases above: a cube MAP records the +X face as its
|
||||
// representative upload target (z = 1, six real faces) and a 1D ARRAY keeps its layer count
|
||||
// in the state-side height (z = 1, N real layers). The cube-map-ARRAY and 3D shapes the
|
||||
// earlier cases use both carry their count in z, so they agree either way and cannot see it.
|
||||
//
|
||||
// The draw goes into a scratch 2D target, never into the layered attachment, so the
|
||||
// materialise path is the only consumer that can fire.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapClearMaterialisedBySamplingReachesEveryFace) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildProgram(nullptr, kCubeSampleFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint cube = MakePoisonedCubeMap();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
|
||||
|
||||
const GLuint layeredFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cube, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube map layered failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered cube-map attachment errored";
|
||||
|
||||
// Consume the queued clear through the sampled-texture path, with no draw into the
|
||||
// layered FBO in between.
|
||||
const GLuint scratchFbo = MakeScratchColorFbo();
|
||||
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
|
||||
DrawSampling(program, scratchFbo, GL_TEXTURE_CUBE_MAP, cube);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
|
||||
|
||||
// Every face, read back through an FBO that names THAT face.
|
||||
//
|
||||
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): measured against a tree
|
||||
// where only +X had been cleared, that spelling returned the cleared colour for all
|
||||
// six faces, so it cannot see per-face state on DirectVulkan and the case built on it
|
||||
// was unfalsifiable. glFramebufferTexture2D + glReadPixels names one face and nothing
|
||||
// else, and the pending clear is long gone by now (materialised and popped above), so
|
||||
// this readback cannot alter what it is measuring.
|
||||
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
const GLuint faceFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading cube face " << kFaceNames[face] << " back errored";
|
||||
ExpectAllCleared(texels, kExtent * kExtent,
|
||||
(std::string("layered GL_TEXTURE_CUBE_MAP glClear materialised by sampling, "
|
||||
"face ") +
|
||||
kFaceNames[face])
|
||||
.c_str());
|
||||
}
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The 1D-array half of the same divergence. Pre-existing rather than introduced by this
|
||||
// branch (the clear copy never had ToVulkanLevelExtent), and fixed by the same hoist.
|
||||
TEST_F(LayeredAttachmentShapeScenario, LayeredOneDArrayClearMaterialisedBySamplingReachesEveryLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint program = BuildProgram(nullptr, kOneDArraySampleFragmentSource);
|
||||
if (program == 0) return;
|
||||
|
||||
const GLuint array = MakePoisoned1DArray();
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
GTEST_SKIP() << "no usable GL_TEXTURE_1D_ARRAY on this backend: " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const GLuint layeredFbo = TrackFramebuffer();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, array, 0);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 1D array layered failed";
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
// The viewport is the LEVEL's shape: a 1D array level is `kExtent` wide and one row
|
||||
// tall, whatever its layer count.
|
||||
glViewport(0, 0, kExtent, 1);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered 1D-array attachment errored";
|
||||
|
||||
const GLuint scratchFbo = MakeScratchColorFbo();
|
||||
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
|
||||
DrawSampling(program, scratchFbo, GL_TEXTURE_1D_ARRAY, array);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
|
||||
|
||||
// GL hands a 1D array back as a two-dimensional image whose ROWS are the layers.
|
||||
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kOneDArrayLayers, Rgba8{});
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, array);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
|
||||
ExpectAllCleared(texels, kExtent,
|
||||
"layered GL_TEXTURE_1D_ARRAY glClear materialised by sampling (unit = layer)");
|
||||
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,404 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitiveRestartScenario.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 - DESKTOP GL_PRIMITIVE_RESTART WITH AN APPLICATION-CHOSEN INDEX.
|
||||
//
|
||||
// Desktop GL restarts on whatever glPrimitiveRestartIndex named; GLES and Vulkan both restart
|
||||
// only on the all-ones value of the index type. DirectGLES used to THROW_EXCEPTION on the
|
||||
// mismatch, and a throw out of a GL entry point unwinds a C++ exception through the C ABI and
|
||||
// kills the process - which is how KHR-GL4x.geometry_shader.primitive_counter.*_rp took the whole
|
||||
// conformance runner down, nine bodies at a time, losing every result in the chunk with it.
|
||||
//
|
||||
// So the first thing this asserts is simply that the process is still here. The second is that
|
||||
// the restart actually happened: the substitution rewrites the index data so the driver restarts
|
||||
// where the application asked, and the difference between "restart honoured" and "restart
|
||||
// silently dropped" is a triangle strip that welds its two halves together across the gap.
|
||||
//
|
||||
// Needs a real context on purpose. The GPU-free suite cannot reach a backend at all, and this is
|
||||
// entirely about what the backend does with the index buffer.
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kSurface = 64;
|
||||
|
||||
const char* const kVertexSource = R"(#version 420 core
|
||||
layout(location = 0) in vec2 a_position;
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 420 core
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Two triangles with a gap down the middle, plus two spare vertices parked at the origin.
|
||||
//
|
||||
// The spares exist so the restart index is a LEGAL vertex index: if the restart were
|
||||
// dropped the driver would still fetch a real vertex rather than read out of bounds, so
|
||||
// the negative case is defined behaviour and the test measures the restart rather than
|
||||
// whatever robust-buffer-access does.
|
||||
constexpr GLfloat kVertices[] = {
|
||||
-0.9f, -0.9f, // 0 - left triangle
|
||||
-0.1f, -0.9f, // 1
|
||||
-0.9f, 0.9f, // 2
|
||||
0.1f, -0.9f, // 3 - right triangle
|
||||
0.9f, -0.9f, // 4
|
||||
0.9f, 0.9f, // 5
|
||||
0.0f, 0.0f, // 6 - spare
|
||||
0.0f, 0.0f, // 7 - spare, and the application's restart index
|
||||
};
|
||||
constexpr GLuint kRestartIndex = 7;
|
||||
|
||||
// A triangle STRIP, restarted in the middle: honoured, it is exactly the two triangles
|
||||
// above. Dropped, the strip welds vertices 2, 7 and 3 into extra triangles that spill
|
||||
// across the gap - which is what the middle probe below catches.
|
||||
constexpr GLuint kIndices[] = {0, 1, 2, kRestartIndex, 3, 4, 5};
|
||||
|
||||
struct Pixel {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
};
|
||||
|
||||
class PrimitiveRestartScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glGenBuffers(1, &m_ebo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
|
||||
glGenTextures(1, &m_colorTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_colorTexture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSurface, kSurface);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTexture, 0);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
|
||||
static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
glViewport(0, 0, kSurface, kSurface);
|
||||
|
||||
m_program = BuildProgram();
|
||||
ASSERT_NE(m_program, 0u) << "the flat-colour program did not build: " << m_buildLog;
|
||||
glUseProgram(m_program);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||
glPrimitiveRestartIndex(0);
|
||||
glUseProgram(0);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||
if (m_colorTexture != 0) glDeleteTextures(1, &m_colorTexture);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
glBindVertexArray(0);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||
m_buildLog = buffer.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// The whole surface, so a failure can report the three probes together rather than
|
||||
// three separate readbacks that might disagree about which draw they saw.
|
||||
std::vector<Pixel> DrawAndRead() {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(kIndices)), GL_UNSIGNED_INT,
|
||||
nullptr);
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
static const Pixel& At(const std::vector<Pixel>& pixels, int x, int y) {
|
||||
return pixels[static_cast<std::size_t>(y) * kSurface + x];
|
||||
}
|
||||
|
||||
static bool IsGreen(const Pixel& p) { return p.g > 128 && p.r < 128; }
|
||||
|
||||
// NDC (-0.5, -0.5): well inside the left triangle whichever way the restart went.
|
||||
static constexpr int kLeftX = 16, kLeftY = 16;
|
||||
// NDC (0.6, -0.5): well inside the right triangle, and outside every welded one.
|
||||
static constexpr int kRightX = 51, kRightY = 16;
|
||||
// NDC (0.2, -0.5): in the gap between the two triangles, and INSIDE the triangle the
|
||||
// strip welds out of vertices 7, 3 and 4 when the restart is dropped. This is the
|
||||
// probe that distinguishes a working restart from a silently ignored one.
|
||||
static constexpr int kGapX = 38, kGapY = 16;
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_ebo = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_colorTexture = 0;
|
||||
GLuint m_program = 0;
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
// THE crash regression. Before the fix this call never returned: DirectGLES threw
|
||||
// std::runtime_error out of glDrawElements and the process died on the spot. Reaching the
|
||||
// assertion at all is most of the point.
|
||||
TEST_F(PrimitiveRestartScenario, AnArbitraryRestartIndexDrawsInsteadOfKillingTheProcess) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "an arbitrary restart index is legal desktop GL and must raise no error";
|
||||
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the gap between the two halves is covered, so the restart was dropped and the "
|
||||
"strip welded across it";
|
||||
}
|
||||
|
||||
// The other half of the state: an application that sets the restart index TO the fixed
|
||||
// all-ones value needs no rewriting at all, and the cap must map straight onto the
|
||||
// driver's own fixed-index restart. Same picture, different path through the backend.
|
||||
TEST_F(PrimitiveRestartScenario, TheFixedIndexValueTakesTheForwardingPath) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// Index 0xFFFFFFFF is not a vertex this draw uses, so the strip is the same shape.
|
||||
const GLuint fixedIndices[] = {0, 1, 2, 0xFFFFFFFFu, 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(fixedIndices), fixedIndices);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0xFFFFFFFFu);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY)));
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)));
|
||||
|
||||
// Put the buffer back for whatever runs next in this fixture.
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(kIndices), kIndices);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// With the cap off, the same index data is just data - nothing restarts, and the strip
|
||||
// welds across the gap. The negative control for the probe above: without it, a backend
|
||||
// that lost the whole draw would pass the test by rendering nothing in the gap.
|
||||
TEST_F(PrimitiveRestartScenario, WithoutTheCapTheStripWeldsAcrossTheGap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glDisable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
const std::vector<Pixel> pixels = DrawAndRead();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the draw itself must still happen";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "with restart disabled the strip is continuous, so the gap must be covered - if "
|
||||
"it is not, the probe above proves nothing";
|
||||
}
|
||||
|
||||
// A second draw with a DIFFERENT restart index has to be rewritten again. The substitution
|
||||
// stages through one scratch buffer, so a cached or half-restored element-array binding
|
||||
// would show up here as the second draw reusing the first one's data.
|
||||
TEST_F(PrimitiveRestartScenario, ChangingTheRestartIndexBetweenDrawsIsHonoured) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
const std::vector<Pixel> restarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_FALSE(IsGreen(At(restarted, kGapX, kGapY)));
|
||||
|
||||
// 6 is the other spare vertex, and it appears nowhere in the index data - so nothing
|
||||
// restarts and the strip is continuous again, from the very same buffer.
|
||||
glPrimitiveRestartIndex(6);
|
||||
const std::vector<Pixel> notRestarted = DrawAndRead();
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(notRestarted, kGapX, kGapY)))
|
||||
<< "the second draw restarted on an index that is not in its data";
|
||||
}
|
||||
|
||||
// A NON-indexed draw has no index stream, so GL primitive restart cannot affect it - and a
|
||||
// list topology is the shape DirectVulkan has to refuse when the device lacks
|
||||
// VK_EXT_primitive_topology_list_restart. Deriving the pipeline's primitiveRestartEnable
|
||||
// from the capability bits alone conflated the two: an application that enables
|
||||
// GL_PRIMITIVE_RESTART once at init and then draws its UI with glDrawArrays(GL_TRIANGLES)
|
||||
// had every one of those draws silently dropped on such a device.
|
||||
TEST_F(PrimitiveRestartScenario, ANonIndexedListTopologyDrawIsUnaffectedByTheCap) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Vertices 0,1,2 are the left triangle; GL_TRIANGLES is a list topology.
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)))
|
||||
<< "primitive restart has no meaning for glDrawArrays, so the draw must render "
|
||||
"normally whatever the device supports";
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 10.3.6 compares the fetched index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX. A restart index the index type cannot hold therefore matches
|
||||
// nothing and the draw restarts NOWHERE - it does not restart on the truncated value, and
|
||||
// it does not restart on the type's all-ones value either, which is what the driver's own
|
||||
// fixed-index restart would have done if it had been left enabled.
|
||||
TEST_F(PrimitiveRestartScenario, ARestartIndexTooLargeForTheIndexTypeRestartsNowhere) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// 16-bit indices with a restart index of 0x10007: the low half (7) IS a real index in
|
||||
// the data, so a truncating comparison would split the strip exactly where a correct
|
||||
// one leaves it whole.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(0x10000u + kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(shortIndices)), GL_UNSIGNED_SHORT,
|
||||
nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "no 16-bit index can equal 0x10007, so nothing restarts and the strip is "
|
||||
"continuous - truncating the restart index to 7 would split it here";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
// The all-ones value of an index type is an ordinary vertex index whenever the array uses
|
||||
// the type's full range, which is exactly why an application picks an arbitrary restart
|
||||
// index in the first place. Substituting the sentinel in place would either steal that
|
||||
// vertex or spuriously restart on it, so the copy widens instead - and the draw has to be
|
||||
// issued with the widened type, which is the part that is easy to forget.
|
||||
TEST_F(PrimitiveRestartScenario, AnAllOnesVertexIndexSurvivesTheSubstitution) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
// The buffer carries the 16-bit all-ones value as an ordinary element. It sits past
|
||||
// the seven indices this draw reads, because the vertex array has only eight entries
|
||||
// and fetching index 65535 would be out of range - what is under test is that its
|
||||
// mere PRESENCE forces the widened copy, and that the draw still finds its own
|
||||
// indices at the right offsets in a copy whose element width has changed underneath
|
||||
// it. Narrowly substituting in place instead would rewrite this element to 0xFFFE.
|
||||
const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5, 0xFFFFu};
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);
|
||||
|
||||
glEnable(GL_PRIMITIVE_RESTART);
|
||||
glPrimitiveRestartIndex(kRestartIndex);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Only the first seven indices are drawn, so the 0xFFFF element is never fetched - what
|
||||
// is under test is that its PRESENCE does not break the substitution or the offsets.
|
||||
glDrawElements(GL_TRIANGLE_STRIP, 7, GL_UNSIGNED_SHORT, nullptr);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
|
||||
std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
|
||||
glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
|
||||
EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
|
||||
EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
|
||||
<< "the restart still has to happen once the copy has been widened";
|
||||
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,227 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RenderbufferBlendFormatScenario.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 - BLENDING WORKS ON A RENDERBUFFER WHOSE GL FORMAT HAS NO EXACT VkFormat.
|
||||
//
|
||||
// DirectVulkan force-disables blending on an attachment whose VkFormat lacks
|
||||
// VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT, which is the right thing to do - blending on such a
|
||||
// format is invalid pipeline state. The probe has to ask about the format the attachment ACTUALLY
|
||||
// has, and for renderbuffers it asked a different question from the one that created the image: the
|
||||
// image comes from ResolveTextureFormatInfo (which widens GL formats with no Vulkan twin onto a real
|
||||
// one) while the probe used the strict 1:1 converter, which answers VK_FORMAT_UNDEFINED for RGBA2,
|
||||
// RGBA12, RGB10, RGB12, RGB16 and the three-channel formats, and the 16-bit packed formats for RGBA4
|
||||
// and RGB5_A1.
|
||||
//
|
||||
// VkFormatProperties for VK_FORMAT_UNDEFINED are all zero, so the probe concluded "not blendable"
|
||||
// and every pipeline for that attachment was built with blendEnable = VK_FALSE - permanently, and
|
||||
// silently apart from one log line. The source colour then overwrites the destination instead of
|
||||
// blending with it, which is a wrong PICTURE, not a wrong error code.
|
||||
//
|
||||
// GL_RGB8 is the ordinary shape and is what this scenario leads with: it is a required
|
||||
// colour-renderable format, its image has been R8G8B8A8_UNORM all along, and the probe asked about
|
||||
// the 24-bit R8G8B8_UNORM that most drivers do not support at all. GL_RGBA4 covers the other half -
|
||||
// a format whose probe answered a real-but-different VkFormat.
|
||||
//
|
||||
// DirectGLES is the control: it forwards the renderbuffer to the ES driver and blends whatever the
|
||||
// driver blends, so a disagreement between the two backends is the defect.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kExtent = 16;
|
||||
|
||||
constexpr const char* kVertexSource = R"(#version 330 core
|
||||
void main()
|
||||
{
|
||||
switch (gl_VertexID)
|
||||
{
|
||||
case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break;
|
||||
case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break;
|
||||
case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break;
|
||||
case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break;
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||
uniform vec4 uColor;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = uColor;
|
||||
}
|
||||
)";
|
||||
|
||||
class RenderbufferBlendFormatScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||
ASSERT_NE(m_program, 0u) << "program did not build: " << error;
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
Destroy();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void Destroy() {
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
if (m_renderbuffer != 0) {
|
||||
glDeleteRenderbuffers(1, &m_renderbuffer);
|
||||
m_renderbuffer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns false (having skipped, not failed) when the driver will not give us a complete
|
||||
// framebuffer for this format - GL only requires a subset of formats to be
|
||||
// colour-renderable, and the point of the scenario is blending, not format support.
|
||||
bool MakeTarget(GLenum internalFormat) {
|
||||
Destroy();
|
||||
glGenRenderbuffers(1, &m_renderbuffer);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, kExtent, kExtent);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
return status == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void DrawColor(float r, float g, float b, float a) {
|
||||
glUseProgram(m_program);
|
||||
glUniform4f(glGetUniformLocation(m_program, "uColor"), r, g, b, a);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
GLuint m_renderbuffer = 0;
|
||||
GLuint m_fbo = 0;
|
||||
GLuint m_vao = 0;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// One draw of opaque black, then a 50%-alpha white draw over it with the ordinary
|
||||
// SRC_ALPHA / ONE_MINUS_SRC_ALPHA function. Blending gives mid-grey; a pipeline built with
|
||||
// blendEnable = VK_FALSE gives white, because the source simply overwrites.
|
||||
//
|
||||
// The tolerance is wide on purpose: RGBA4 has four bits per channel, so "mid-grey" is one of
|
||||
// a handful of representable values and the test must not become a quantisation test.
|
||||
void ExpectBlendedRatherThanOverwritten(const char* what) {
|
||||
const Image image = ReadPixels(kExtent, kExtent);
|
||||
ASSERT_FALSE(image.Empty()) << what;
|
||||
const Rgba8 centre = image.At(kExtent / 2, kExtent / 2);
|
||||
EXPECT_GT(int(centre.r), 40) << what << ": got " << centre << ", which is darker than a blend of "
|
||||
"black and 50% white";
|
||||
EXPECT_LT(int(centre.r), 215) << what << ": got " << centre
|
||||
<< ", which is the source colour - blending was disabled";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The ordinary case, and the one broken today rather than only after the format table was
|
||||
// unified: a three-channel colour renderbuffer. Its image has been R8G8B8A8_UNORM all along while
|
||||
// the blend probe asked about R8G8B8_UNORM, which most drivers do not support at all.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAThreeChannelRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGB8)) GTEST_SKIP() << "GL_RGB8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGB8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The other half: a format whose strict converter answers a real-but-different VkFormat
|
||||
// (R4G4B4A4_UNORM_PACK16) while the image is R8G8B8A8_UNORM. Blend support for the packed 16-bit
|
||||
// formats is optional in Vulkan, so the probe could legitimately answer "no" for a format the
|
||||
// attachment does not have.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnALowBitPackedRenderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA4)) GTEST_SKIP() << "GL_RGBA4 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA4");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
// The control that keeps both of the above honest: the same sequence on the format whose probe
|
||||
// and image always agreed. If this one ever fails, the scenario is measuring the blend setup
|
||||
// rather than the format resolution.
|
||||
TEST_F(RenderbufferBlendFormatScenario, BlendingWorksOnAnRgba8Renderbuffer) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
if (!MakeTarget(GL_RGBA8)) GTEST_SKIP() << "GL_RGBA8 renderbuffer is not framebuffer-complete here";
|
||||
|
||||
glViewport(0, 0, kExtent, kExtent);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
DrawColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
DrawColor(1.0f, 1.0f, 1.0f, 0.5f);
|
||||
glDisable(GL_BLEND);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the blended draw left a GL error behind";
|
||||
|
||||
ExpectBlendedRatherThanOverwritten("GL_RGBA8");
|
||||
Gl().EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,274 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleVariablesScenario.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 - gl_NumSamples REACHES THE SHADER, AND IT FOLLOWS THE DRAW FRAMEBUFFER.
|
||||
//
|
||||
// glslang declares gl_NumSamples only when it is NOT targeting SPIR-V - both the desktop and the
|
||||
// ES branch of Initialize.cpp wrap `uniform int gl_NumSamples;` in `if (spvVersion.spv == 0)`,
|
||||
// because SPIR-V has no NumSamples builtin to lower it to - and MobileGL always targets SPIR-V.
|
||||
// Every fragment shader that read the built-in therefore died at COMPILE time with
|
||||
// "'gl_NumSamples' : undeclared identifier", which is all 144 KHR-GL46.sample_variables.mask.*
|
||||
// bodies plus their es_31_compatibility twins.
|
||||
//
|
||||
// The source pipeline now lowers it onto a reserved default-block uniform and the draw path writes
|
||||
// the current draw framebuffer's sample count into it. Two claims, and the second is the one a
|
||||
// compile-only test cannot make: the value must be the DRAW FRAMEBUFFER's, so one program drawn
|
||||
// into a multisample target and then into a single-sample target has to report both counts. A
|
||||
// link-time bake would pass the first assertion and fail the second, which is exactly why the
|
||||
// write lives per draw.
|
||||
//
|
||||
// llvmpipe and lavapipe both offer 4x multisample RGBA8, so this runs for real in CI rather than
|
||||
// skipping; the skips below are for a driver that offers no multisample renderbuffer at all.
|
||||
|
||||
#include <algorithm>
|
||||
#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 400 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// gl_NumSamples scaled so each count lands on its own well-separated 8-bit value: 1 -> 16,
|
||||
// 2 -> 32, 4 -> 64. Every sample of the fragment gets the same colour, so the resolve blit
|
||||
// averages identical values and the readback is exact rather than approximate.
|
||||
constexpr const char* kFS = R"(#version 400 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(float(gl_NumSamples) * (16.0 / 255.0), 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class SampleVariablesScenario : public ScenarioTest {};
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SampleVariablesScenario, GlNumSamplesFollowsTheDrawFramebuffersSampleCount) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
// The compile failure this scenario exists for lands here, with glslang's own text.
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
|
||||
}
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// ---- multisample target ----
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
// What the driver actually allocated - a request is a lower bound, and the shader has to
|
||||
// agree with the query rather than with what was asked for.
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
ASSERT_GE(realizedSamples, 2) << "the multisample framebuffer reports GL_SAMPLES " << realizedSamples;
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Resolve into the default framebuffer to read it back.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0) << " into a " << realizedSamples
|
||||
<< "-sample framebuffer; 1 means the reserved uniform was never written, 0 means it was "
|
||||
<< "written but never uploaded";
|
||||
}
|
||||
gl.EndFrame();
|
||||
|
||||
// ---- the SAME program into a single-sample target ----
|
||||
// A link-time bake of the sample count would keep reporting the multisample value here.
|
||||
GLuint ssFbo = 0, ssRbo = 0;
|
||||
glGenFramebuffers(1, &ssFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, ssFbo);
|
||||
glGenRenderbuffers(1, &ssRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, ssRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ssRbo);
|
||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
{
|
||||
const Image single = ReadPixels(width, height);
|
||||
const Rgba8 centre = single.At(width / 2, height / 2);
|
||||
// GL 4.6 core 15.2.2: gl_NumSamples is ONE for a non-multisample framebuffer, where
|
||||
// glGetIntegerv(GL_SAMPLES) answers zero.
|
||||
EXPECT_NEAR(centre.r, 16, 2)
|
||||
<< "gl_NumSamples read " << (centre.r / 16.0)
|
||||
<< " into a single-sample framebuffer; the value is a property of the DRAW FRAMEBUFFER, "
|
||||
<< "so re-using the program must re-write it";
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteRenderbuffers(1, &ssRbo);
|
||||
glDeleteFramebuffers(1, &ssFbo);
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// ARB_sample_shading is advertised, and until now glMinSampleShading was a logging no-op while
|
||||
// glEnable(GL_SAMPLE_SHADING) fell out of RenderState::SetCapability's default arm - so an
|
||||
// application could ask for a shading rate and get silence from both halves.
|
||||
//
|
||||
// What this can and cannot assert. The RATE itself is not observable from a portable shader:
|
||||
// GL 4.6 core 14.3.1 makes any use of gl_SampleID or gl_SamplePosition force per-sample
|
||||
// evaluation on its own, so the very built-ins that would report the rate defeat the
|
||||
// measurement. What IS worth pinning is that the state now reaches both backends without
|
||||
// damage: DirectGLES forwards glEnable(GL_SAMPLE_SHADING) + glMinSampleShading to the ES
|
||||
// driver (and must not, on a driver that has neither, push an INVALID_ENUM into the
|
||||
// application's error queue), and DirectVulkan bakes sampleShadingEnable/minSampleShading into
|
||||
// a NEW pipeline - which it may only do with the device's sampleRateShading feature enabled.
|
||||
TEST_F(SampleVariablesScenario, SampleShadingStateReachesTheBackendWithoutDisturbingTheDraw) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
GLint maxSamples = 0;
|
||||
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
|
||||
if (requestedSamples < 2) {
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; sample shading needs a multisample target";
|
||||
}
|
||||
|
||||
GLuint msFbo = 0, msRbo = 0;
|
||||
glGenFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
||||
glGenRenderbuffers(1, &msRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteProgram(program);
|
||||
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
|
||||
}
|
||||
|
||||
GLint realizedSamples = 0;
|
||||
glGetIntegerv(GL_SAMPLES, &realizedSamples);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glViewport(0, 0, width, height);
|
||||
|
||||
glEnable(GL_SAMPLE_SHADING);
|
||||
glMinSampleShading(1.0f);
|
||||
EXPECT_EQ(glIsEnabled(GL_SAMPLE_SHADING), static_cast<GLboolean>(GL_TRUE));
|
||||
GLfloat rate = -1.0f;
|
||||
glGetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &rate);
|
||||
EXPECT_FLOAT_EQ(rate, 1.0f);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "enabling sample shading raised a GL error";
|
||||
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
DrawFullViewportQuad(program);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the sample-shading draw raised a GL error";
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
const Image resolved = ReadPixels(width, height);
|
||||
const Rgba8 centre = resolved.At(width / 2, height / 2);
|
||||
// The rate changes how OFTEN the shader runs, never what it computes - so the same
|
||||
// gl_NumSamples reading has to come back.
|
||||
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
|
||||
<< "the draw changed its result once sample shading was enabled";
|
||||
|
||||
glMinSampleShading(0.0f);
|
||||
glDisable(GL_SAMPLE_SHADING);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glDeleteRenderbuffers(1, &msRbo);
|
||||
glDeleteFramebuffers(1, &msFbo);
|
||||
glDeleteProgram(program);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,339 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.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 - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END.
|
||||
//
|
||||
// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and
|
||||
// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments,
|
||||
// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM.
|
||||
// Every gl_spirv conformance body died on the first of those two calls.
|
||||
//
|
||||
// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two
|
||||
// modules that glslang compiled ahead of time (embedded below as words, so the test depends on
|
||||
// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel
|
||||
// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe.
|
||||
//
|
||||
// The two specialization constants are the load-bearing part. The vertex module scales its
|
||||
// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant
|
||||
// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a
|
||||
// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of
|
||||
// green), and BOTH would show up in the readback. A "specialization" that merely stored the
|
||||
// values without folding them in is exactly the failure mode this shape is built to catch.
|
||||
//
|
||||
// The GLSL the modules came from:
|
||||
// vertex: layout(location = 0) in vec2 aPos;
|
||||
// layout(constant_id = 3) const float uScale = 1.0;
|
||||
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
|
||||
// fragment: layout(location = 0) out vec4 oColor;
|
||||
// layout(constant_id = 7) const int uChannel = 0;
|
||||
// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; }
|
||||
// compiled with `glslangValidator -G --target-env opengl`.
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V
|
||||
#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551
|
||||
#endif
|
||||
#ifndef GL_SPIR_V_BINARY
|
||||
#define GL_SPIR_V_BINARY 0x9552
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
class SpirvShaderBinaryScenario : public ScenarioTest {};
|
||||
|
||||
// 255 words
|
||||
const unsigned int kVertexModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
|
||||
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
|
||||
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
|
||||
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
|
||||
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
|
||||
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
|
||||
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
|
||||
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
|
||||
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
|
||||
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
|
||||
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
|
||||
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
|
||||
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
|
||||
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
|
||||
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
|
||||
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
|
||||
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
|
||||
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
|
||||
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
|
||||
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
|
||||
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
|
||||
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
|
||||
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
|
||||
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
|
||||
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
|
||||
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
|
||||
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
|
||||
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
// 134 words
|
||||
const unsigned int kFragmentModule[] = {
|
||||
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
|
||||
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
|
||||
0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u,
|
||||
0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
|
||||
0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u,
|
||||
0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u,
|
||||
0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
|
||||
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
|
||||
0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u,
|
||||
0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au,
|
||||
0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u,
|
||||
0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u,
|
||||
0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u,
|
||||
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
|
||||
0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u,
|
||||
0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u,
|
||||
0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
|
||||
|
||||
// The quad the vertex module transforms. Full-viewport before the scale, so a scale of
|
||||
// 0.5 covers exactly the middle half of each axis and the corners stay background.
|
||||
const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
|
||||
// The specialization constant ids the two modules declare.
|
||||
constexpr unsigned int kScaleConstantId = 3;
|
||||
constexpr unsigned int kChannelConstantId = 7;
|
||||
|
||||
unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount,
|
||||
unsigned int constantId, unsigned int constantValue, std::string* outLog) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words,
|
||||
static_cast<GLsizei>(wordCount * sizeof(unsigned int)));
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
if (outLog) *outLog = "glShaderBinary rejected the module";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLint isSpirv = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) {
|
||||
if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary";
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
glSpecializeShader(shader, "main", 1, &constantId, &constantValue);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled != GL_TRUE) {
|
||||
if (outLog) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
*outLog = std::string(log.data());
|
||||
}
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) {
|
||||
if (!Ready()) return;
|
||||
|
||||
GLint formatCount = -1;
|
||||
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format";
|
||||
|
||||
std::vector<GLint> formats(static_cast<size_t>(formatCount), 0);
|
||||
glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
|
||||
<< "the count and the list have to describe the same thing";
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
// 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but
|
||||
// GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently.
|
||||
glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule));
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_ENUM));
|
||||
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION))
|
||||
<< "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader";
|
||||
|
||||
// glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with
|
||||
// it - the transition the conformance suite checks explicitly.
|
||||
const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
GLint isSpirv = GL_TRUE;
|
||||
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
|
||||
EXPECT_EQ(isSpirv, GL_FALSE);
|
||||
glCompileShader(shader);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializeShaderErrorSurfaceMatchesTheExtension) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
ASSERT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// 4242 is not one of the module's constant ids. ARB_gl_spirv enumerates that as
|
||||
// GL_INVALID_VALUE, and an erroring GL command has no other effect - so the shader is left
|
||||
// untouched rather than pushed into a failed-compile state.
|
||||
const unsigned int badId = 4242;
|
||||
const unsigned int value = 0;
|
||||
glSpecializeShader(shader, "main", 1, &badId, &value);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Same for an entry point the module does not carry.
|
||||
glSpecializeShader(shader, "notMain", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_VALUE));
|
||||
|
||||
// Neither refusal specialized the shader, so a well-formed call still works.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
EXPECT_EQ(compiled, GL_TRUE);
|
||||
|
||||
// But a SECOND specialization of a shader that HAS been specialized is INVALID_OPERATION
|
||||
// until glShaderBinary re-associates the module.
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
||||
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
|
||||
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "re-associating the module makes specialization legal again";
|
||||
|
||||
glDeleteShader(shader);
|
||||
}
|
||||
|
||||
TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 16);
|
||||
ASSERT_GE(height, 16);
|
||||
|
||||
std::string log;
|
||||
// Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies.
|
||||
unsigned int halfBits = 0;
|
||||
const float half = 0.5f;
|
||||
std::memcpy(&halfBits, &half, sizeof(halfBits));
|
||||
|
||||
const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule,
|
||||
sizeof(kVertexModule) / sizeof(kVertexModule[0]),
|
||||
kScaleConstantId, halfBits, &log);
|
||||
ASSERT_NE(vs, 0u) << "vertex: " << log;
|
||||
// Channel 1 is green; the module's own default is 0 (red), so a specialization that did
|
||||
// nothing paints the wrong colour.
|
||||
const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule,
|
||||
sizeof(kFragmentModule) / sizeof(kFragmentModule[0]),
|
||||
kChannelConstantId, 1u, &log);
|
||||
ASSERT_NE(fs, 0u) << "fragment: " << log;
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> programLog(static_cast<size_t>(length > 0 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(programLog.size()), nullptr, programLog.data());
|
||||
FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data();
|
||||
}
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Image painted = ReadPixels(width, height);
|
||||
const Rgba8 centre = painted.At(width / 2, height / 2);
|
||||
EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied";
|
||||
EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted";
|
||||
|
||||
// A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the
|
||||
// clear colour - which is what proves constant id 3 reached the vertex module. At the
|
||||
// default scale of 1.0 the quad covers the whole viewport and this pixel would be green.
|
||||
const Rgba8 corner = painted.At(1, 1);
|
||||
EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied";
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -19,7 +19,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
BufferTarget::DrawIndirect, BufferTarget::Parameter, BufferTarget::ShaderStorage);
|
||||
constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback,
|
||||
BufferTarget::AtomicCounter, BufferTarget::ShaderStorage);
|
||||
constexpr SizeT BufferBindingPointCount = 36;
|
||||
// How many indexed binding points each of BufferBindPointTargets gets. 84 is the GL 4.5 core
|
||||
// minimum for GL_MAX_UNIFORM_BUFFER_BINDINGS (table 23.64) and this array is the capacity
|
||||
// that limit is clamped against - at 36 the clamp in GL_Getter was degenerate (lo == hi) and
|
||||
// no application could ever be told about, or bind to, a binding point past the 36th. The
|
||||
// other three targets advertise their own, smaller ceilings out of
|
||||
// GetIndexedBufferQueryPointCount, so widening this does not widen what they promise; it only
|
||||
// costs the unused tail of three arrays.
|
||||
constexpr SizeT BufferBindingPointCount = 84;
|
||||
|
||||
class BufferState {
|
||||
public:
|
||||
|
||||
@@ -650,6 +650,12 @@ namespace MobileGL::MG_State {
|
||||
// a graphics program carrying a compute module, which Adreno 830 does not reject
|
||||
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
|
||||
Bool anyStage = false;
|
||||
// Which stages the composite ACTUALLY got a shader for. Not the same question as
|
||||
// "which stages have a stage program bound": one program bound with
|
||||
// GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
|
||||
// stages it was linked with. The transform-feedback capture stage is chosen off this,
|
||||
// because it has to be the stage that will exist in the composite's own link.
|
||||
Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
@@ -665,9 +671,57 @@ namespace MobileGL::MG_State {
|
||||
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
|
||||
composite->AttachShaderWithPinnedLinkInput(ref);
|
||||
anyStage = true;
|
||||
compositeHasStage[stage] = true;
|
||||
}
|
||||
}
|
||||
if (!anyStage) return nullProgram;
|
||||
// Transform feedback captures the output of the LAST vertex-processing stage
|
||||
// (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
|
||||
// only the stage program carrying that stage can have been given. The composite is
|
||||
// assembled out of the stage programs' shaders and inherits none of their
|
||||
// GL-thread-owned state, so without this it links with an empty capture list and
|
||||
// glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
|
||||
// no transform feedback varyings") even though glValidateProgramPipeline had passed.
|
||||
//
|
||||
// TWO RULES, both easy to get subtly wrong and both load-bearing:
|
||||
//
|
||||
// (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
|
||||
// take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
|
||||
// deliberately bumps no version - so a request written after the stage program's
|
||||
// last link is invisible to the composite cache's signature yet would be picked up
|
||||
// by the next rebuild, making the capture list depend on whether some unrelated
|
||||
// event happened to invalidate the cache. Worse, a name that is not an output of
|
||||
// the capture stage fails the composite's OWN link, and a failed composite makes
|
||||
// every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
|
||||
// snapshot removes the whole class: linked state only moves at a link, and a link
|
||||
// is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
|
||||
// the existing cache key is sufficient by construction.
|
||||
// GetTransformFeedbackInterfaceNames() is the right accessor rather than the
|
||||
// resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
|
||||
// (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
|
||||
//
|
||||
// (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
|
||||
// the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
|
||||
// getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
|
||||
// WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
|
||||
// Skipping a geometry stage that has no capture list and installing the vertex
|
||||
// stage's instead made them disagree, and the composite then resolved a vertex
|
||||
// program's names against the geometry intermediate - capturing where GL says it
|
||||
// must not, or failing the link and killing every draw. A capture stage with an
|
||||
// empty list is not a reason to look further down: it is the answer, and
|
||||
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
|
||||
for (const ShaderStage captureStage:
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
|
||||
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
|
||||
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
|
||||
if (!captureProgram) continue;
|
||||
const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
|
||||
if (!linkedNames.empty()) {
|
||||
composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
|
||||
captureProgram->GetTransformFeedbackBufferMode());
|
||||
}
|
||||
break;
|
||||
}
|
||||
// A pipeline with no fragment stage still rasterises, so the default fragment
|
||||
// shader is wanted here even though the separable stage programs never get one.
|
||||
composite->Link(true);
|
||||
@@ -812,6 +866,22 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetPatchVertices(vertices);
|
||||
}
|
||||
|
||||
void GLContext::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
|
||||
m_renderState.SetPatchDefaultOuterLevel(levels);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetPatchDefaultOuterLevel() const {
|
||||
return m_renderState.GetPatchDefaultOuterLevel();
|
||||
}
|
||||
|
||||
void GLContext::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
|
||||
m_renderState.SetPatchDefaultInnerLevel(levels);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetPatchDefaultInnerLevel() const {
|
||||
return m_renderState.GetPatchDefaultInnerLevel();
|
||||
}
|
||||
|
||||
Uint GLContext::GetPatchVertices() const {
|
||||
return m_renderState.GetPatchVertices();
|
||||
}
|
||||
@@ -832,6 +902,26 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetPolygonOffsetUnits();
|
||||
}
|
||||
|
||||
void GLContext::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
|
||||
m_renderState.SetPolygonOffsetClamped(factor, units, clamp);
|
||||
}
|
||||
|
||||
Float GLContext::GetPolygonOffsetClamp() const {
|
||||
return m_renderState.GetPolygonOffsetClamp();
|
||||
}
|
||||
|
||||
void GLContext::SetClipControl(GLenum origin, GLenum depth) {
|
||||
m_renderState.SetClipControl(origin, depth);
|
||||
}
|
||||
|
||||
GLenum GLContext::GetClipOrigin() const {
|
||||
return m_renderState.GetClipOrigin();
|
||||
}
|
||||
|
||||
GLenum GLContext::GetClipDepthMode() const {
|
||||
return m_renderState.GetClipDepthMode();
|
||||
}
|
||||
|
||||
void GLContext::SetCapability(CapabilityInput cap, Bool enabled) {
|
||||
m_renderState.SetCapability(cap, enabled);
|
||||
}
|
||||
@@ -1009,6 +1099,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetSampleMaskValue();
|
||||
}
|
||||
|
||||
void GLContext::SetMinSampleShadingValue(Float value) {
|
||||
m_renderState.SetMinSampleShadingValue(value);
|
||||
}
|
||||
|
||||
Float GLContext::GetMinSampleShadingValue() const {
|
||||
return m_renderState.GetMinSampleShadingValue();
|
||||
}
|
||||
|
||||
void GLContext::SetPixelStoreParam(PixelStoreParam param, Int value) {
|
||||
m_renderState.SetPixelStoreParam(param, value);
|
||||
}
|
||||
|
||||
@@ -213,9 +213,18 @@ namespace MobileGL {
|
||||
Float GetPointSize() const;
|
||||
void SetPatchVertices(Uint vertices);
|
||||
Uint GetPatchVertices() const;
|
||||
void SetPatchDefaultOuterLevel(const FloatVec4& levels);
|
||||
const FloatVec4& GetPatchDefaultOuterLevel() const;
|
||||
void SetPatchDefaultInnerLevel(const FloatVec2& levels);
|
||||
const FloatVec2& GetPatchDefaultInnerLevel() const;
|
||||
void SetPolygonOffset(Float factor, Float units);
|
||||
void SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
|
||||
Float GetPolygonOffsetFactor() const;
|
||||
Float GetPolygonOffsetUnits() const;
|
||||
Float GetPolygonOffsetClamp() const;
|
||||
void SetClipControl(GLenum origin, GLenum depth);
|
||||
GLenum GetClipOrigin() const;
|
||||
GLenum GetClipDepthMode() const;
|
||||
void SetHint(GLenum target, GLenum mode);
|
||||
GLenum GetHint(GLenum target) const;
|
||||
void SetPointFadeThresholdSize(Float size);
|
||||
@@ -276,6 +285,8 @@ namespace MobileGL {
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
void SetSampleMaskValue(Uint32 mask);
|
||||
Uint32 GetSampleMaskValue() const;
|
||||
void SetMinSampleShadingValue(Float value);
|
||||
Float GetMinSampleShadingValue() const;
|
||||
void SetPixelStoreParam(PixelStoreParam param, Int value);
|
||||
Int GetPixelStoreParam(PixelStoreParam param) const;
|
||||
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -29,13 +30,11 @@ namespace {
|
||||
// capacity, which is also the width of the Uint32 masks backends build from it.
|
||||
static MobileGL::Int GetReflectionVertexAttribLimit(
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
constexpr MobileGL::Int capacity =
|
||||
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!env.HasBackend()) return capacity;
|
||||
|
||||
const MobileGL::Int backendLimit = env.params.MaxVertexAttribs;
|
||||
if (backendLimit <= 0) return capacity;
|
||||
return std::min(backendLimit, capacity);
|
||||
// One shared definition with glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) and with
|
||||
// BuildTBuiltInResource's gl_MaxVertexAttribs - the three used to carry three copies of
|
||||
// this formula and glslang's copy was a hardcoded 64.
|
||||
return MobileGL::MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(env.HasBackend(),
|
||||
env.params.MaxVertexAttribs);
|
||||
}
|
||||
|
||||
// Everything the post-link query surface ever asks a glslang::TType, flattened into a
|
||||
@@ -622,13 +621,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
// mapper's collect callback is the last point at which a resource's qualifier still
|
||||
// says what the SHADER declared rather than what glslang assigned, so both captures
|
||||
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
|
||||
// The binding-range rule (GLSL 4.30 4.4.5): its ceilings in, and the first violation the
|
||||
// resolver finds out. Enforced at the link because mapIO's collect callback is the last
|
||||
// point at which a resource's qualifier still says what the SHADER declared - see
|
||||
// TMglGlslIoResolver::CheckDeclaredBindingRange.
|
||||
String resourceBindingViolation;
|
||||
ProgramAttrib attrib{.shaders = Move(shaders),
|
||||
.explicitVertexInLocations = in.explicitAttribLocations,
|
||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
|
||||
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
|
||||
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding};
|
||||
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding,
|
||||
.resourceBindingLimits = in.env ? ResolveResourceBindingLimits(*in.env)
|
||||
: MG_Util::ShaderTranspiler::ResourceBindingLimits{},
|
||||
.resourceBindingViolation = &resourceBindingViolation};
|
||||
|
||||
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
||||
auto result = ShaderCompiler::LinkProgram(attrib);
|
||||
@@ -672,9 +679,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it
|
||||
// (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one.
|
||||
// The geometry stage's link properties. GL_GEOMETRY_INPUT_TYPE is load-bearing beyond the
|
||||
// query surface - a draw's primitive type has to be compatible with it (GL 4.6 core
|
||||
// 11.3.1) - so this block runs for every link, not only a capturing one. The other three
|
||||
// are pure glGetProgramiv answers that previously had no source at all.
|
||||
artifacts.gsInputPrimitive = GL_NONE;
|
||||
artifacts.gsOutputPrimitive = GL_NONE;
|
||||
artifacts.gsMaxVertices = 0;
|
||||
artifacts.gsInvocations = 0;
|
||||
if (const glslang::TIntermediate* gs = artifacts.program->getIntermediate(EShLangGeometry)) {
|
||||
switch (gs->getInputPrimitive()) {
|
||||
case glslang::ElgPoints: artifacts.gsInputPrimitive = GL_POINTS; break;
|
||||
@@ -684,6 +696,77 @@ namespace MobileGL::MG_State::GLState {
|
||||
case glslang::ElgTrianglesAdjacency: artifacts.gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break;
|
||||
default: break;
|
||||
}
|
||||
switch (gs->getOutputPrimitive()) {
|
||||
case glslang::ElgPoints: artifacts.gsOutputPrimitive = GL_POINTS; break;
|
||||
case glslang::ElgLineStrip: artifacts.gsOutputPrimitive = GL_LINE_STRIP; break;
|
||||
case glslang::ElgTriangleStrip: artifacts.gsOutputPrimitive = GL_TRIANGLE_STRIP; break;
|
||||
default: break;
|
||||
}
|
||||
// glslang leaves both at TQualifier::layoutNotSet (-1) when the shader declared no
|
||||
// such layout, and `invocations` defaults to one per GLSL 4.60 4.4.2.2 - so clamp
|
||||
// rather than forward, or GL_GEOMETRY_SHADER_INVOCATIONS reports the sentinel.
|
||||
artifacts.gsMaxVertices = std::max(gs->getVertices(), 0);
|
||||
artifacts.gsInvocations = std::max(gs->getInvocations(), 1);
|
||||
}
|
||||
|
||||
// The tessellation evaluation stage's link properties, GL 4.6 core table 23.35: the
|
||||
// primitive generator's mode, spacing, winding and point mode. (The control stage's
|
||||
// output patch size is captured below, together with the limit check that goes with it.)
|
||||
artifacts.tessGenMode = GL_NONE;
|
||||
artifacts.tessGenSpacing = GL_NONE;
|
||||
artifacts.tessGenVertexOrder = GL_NONE;
|
||||
artifacts.tessGenPointMode = false;
|
||||
if (const glslang::TIntermediate* tes = artifacts.program->getIntermediate(EShLangTessEvaluation)) {
|
||||
switch (tes->getInputPrimitive()) {
|
||||
case glslang::ElgTriangles: artifacts.tessGenMode = GL_TRIANGLES; break;
|
||||
case glslang::ElgQuads: artifacts.tessGenMode = GL_QUADS; break;
|
||||
case glslang::ElgIsolines: artifacts.tessGenMode = GL_ISOLINES; break;
|
||||
default: break;
|
||||
}
|
||||
// GLSL 4.60 4.4.2.3: equal_spacing and ccw are the defaults, which is what an unset
|
||||
// qualifier means here.
|
||||
switch (tes->getVertexSpacing()) {
|
||||
case glslang::EvsFractionalEven: artifacts.tessGenSpacing = GL_FRACTIONAL_EVEN; break;
|
||||
case glslang::EvsFractionalOdd: artifacts.tessGenSpacing = GL_FRACTIONAL_ODD; break;
|
||||
default: artifacts.tessGenSpacing = GL_EQUAL; break;
|
||||
}
|
||||
switch (tes->getVertexOrder()) {
|
||||
case glslang::EvoCw: artifacts.tessGenVertexOrder = GL_CW; break;
|
||||
default: artifacts.tessGenVertexOrder = GL_CCW; break;
|
||||
}
|
||||
artifacts.tessGenPointMode = tes->getPointMode();
|
||||
}
|
||||
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES, i.e. the `layout(vertices = N) out` the control stage
|
||||
// declared, and the limit that goes with it.
|
||||
//
|
||||
// GL 4.6 core 11.2.1.1: the LINK fails when N is greater than MAX_PATCH_VERTICES. Nothing
|
||||
// enforced it - glslang's layout handling only rejects N <= 0 (ParseHelper.cpp "must be
|
||||
// greater than 0") and carries maxPatchVertices in TBuiltInResource purely so
|
||||
// gl_MaxPatchVertices can expand from it, exactly the gap ValidateImageUniformLimits
|
||||
// documents for image uniforms. Checked at LINK rather than at compile on purpose: the CTS
|
||||
// requires the offending shader to COMPILE ("Compilation passed as allowed") and only the
|
||||
// link to fail, and turning it into a parse error would newly break an application that
|
||||
// compiles such a shader and never links it.
|
||||
//
|
||||
// The limit is the one glGetIntegerv answers (GL_Getter.cpp reads the same
|
||||
// DynamicBackendParameters field), so the advertised number and the enforced number cannot
|
||||
// drift apart.
|
||||
artifacts.tcsOutputVertices = 0;
|
||||
if (const glslang::TIntermediate* tcs = artifacts.program->getIntermediate(EShLangTessControl)) {
|
||||
artifacts.tcsOutputVertices = static_cast<Int>(tcs->getVertices());
|
||||
if (artifacts.tcsOutputVertices > env.params.MaxPatchVertices) {
|
||||
artifacts.linkStatus = false;
|
||||
// Same invariant as the compute local-size gate above: a rejected link leaves no
|
||||
// TProgram behind for a query surface to find.
|
||||
artifacts.program.reset();
|
||||
artifacts.infoLog = std::format(
|
||||
"Tessellation control shader declares an output patch of {} vertices, more than the {} "
|
||||
"GL_MAX_PATCH_VERTICES allows.",
|
||||
artifacts.tcsOutputVertices, env.params.MaxPatchVertices);
|
||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
|
||||
@@ -1104,6 +1187,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
// The gl_NumSamples stand-in InjectNumSamplesBuiltinShim declared. It is a driver
|
||||
// uniform, not the application's: gl_NumSamples is a BUILT-IN, so a conformant
|
||||
// implementation reports nothing for it in GL_ACTIVE_UNIFORMS, glGetActiveUniform or
|
||||
// glGetUniformLocation, and nothing may write it through glUniform* either. Filtering
|
||||
// it here does both, and costs it no storage: BuildGlobalUboRouting takes its offset
|
||||
// from the SPIR-V metadata by name, not from the GL location space.
|
||||
if (isGlobalUboMember(uniform) &&
|
||||
uniform.name == MG_Util::ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) {
|
||||
artifacts.usesReservedNumSamples = true;
|
||||
MGLOG_D("ProgramObject %u: Reflection - reserved gl_NumSamples stand-in '%s' hidden from the GL "
|
||||
"uniform surface",
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
if (isBufferVariable(uniform)) {
|
||||
MGLOG_D("ProgramObject %u: Reflection - buffer variable '%s' filtered from the GL uniform "
|
||||
"surface",
|
||||
@@ -1595,6 +1692,25 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
|
||||
artifacts.uniformBlockBinding[i] =
|
||||
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
|
||||
// The second way a binding reaches the state layer's indexed-binding array, and the
|
||||
// one glUniformBlockBinding's new bound cannot see. glslang does not range-check a
|
||||
// uniform block's layout(binding = N) against anything - TBuiltInResource has no
|
||||
// maxUniformBufferBindings field at all, and ParseHelper bounds only samplers and
|
||||
// atomic counters - so `layout(binding = 5000) uniform Blk {...}` compiled and linked
|
||||
// clean and then had both backends subscript the array at 5000 on the first draw.
|
||||
// Stated against the same ceiling glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS)
|
||||
// advertises; an instance array whose LAST element passes it is a link error even
|
||||
// though its base fits, same rule as the explicit-location check above.
|
||||
if (artifacts.uniformBlockBinding[i] >=
|
||||
static_cast<Int>(MG_State::GLState::BufferBindingPointCount)) {
|
||||
artifacts.infoLog =
|
||||
std::format("Uniform block '{}' declares binding {}, which is not less than "
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS ({}).",
|
||||
ubo.name, artifacts.uniformBlockBinding[i],
|
||||
static_cast<Int>(MG_State::GLState::BufferBindingPointCount));
|
||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||
return false;
|
||||
}
|
||||
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
||||
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
||||
}
|
||||
|
||||
@@ -492,6 +492,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// time, for anything cached during the pending window itself.)
|
||||
++m_backendStateVersion;
|
||||
BumpLinkObservableVersions();
|
||||
// The separable flag takes effect HERE, at the link, and nowhere else (GL 4.6 core 7.3).
|
||||
// Latched before the early-outs below so a link that fails still counts as a link -
|
||||
// what must not update it is a link that never happened at all.
|
||||
m_linkedSeparable = m_separable;
|
||||
// A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this
|
||||
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
||||
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
||||
@@ -536,6 +540,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
|
||||
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
|
||||
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
|
||||
// ARB_gl_spirv: a program built from SPIR-V declares its transform feedback through
|
||||
// XfbBuffer/XfbStride/Offset DECORATIONS, and glTransformFeedbackVaryings has no effect on
|
||||
// it at all. glSpecializeShader translated those decorations into the equivalent name
|
||||
// request (ShaderCompiler::SpecializeAndDecompileSpirvModule), and this is where it enters
|
||||
// the link - so everything downstream, the frontend packer and both backends, sees one
|
||||
// declaration form instead of two.
|
||||
//
|
||||
// The capture stage is the LAST vertex-processing stage the program has, which is the same
|
||||
// rule ProgramLinkTask::ResolveTransformFeedbackVaryings resolves the names against. The
|
||||
// application's own request wins if it made one: that can only happen on a mixed program,
|
||||
// which is not a shape ARB_gl_spirv defines, and honouring what the application explicitly
|
||||
// asked for is the safer of the two readings.
|
||||
if (task->in.requestedXfbVaryings.empty()) {
|
||||
for (const ShaderStage captureStage:
|
||||
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
|
||||
Bool stagePresent = false;
|
||||
for (const auto& shader : m_shaders) {
|
||||
if (!shader || shader->GetShaderStage() != captureStage) continue;
|
||||
stagePresent = true;
|
||||
if (shader->GetSpirvXfbVaryings().empty()) continue;
|
||||
task->in.requestedXfbVaryings = shader->GetSpirvXfbVaryings();
|
||||
task->in.requestedXfbBufferMode = shader->GetSpirvXfbBufferMode();
|
||||
break;
|
||||
}
|
||||
if (stagePresent) break;
|
||||
}
|
||||
}
|
||||
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
|
||||
|
||||
Vector<SharedPtr<ShaderCompileTask>> deps;
|
||||
|
||||
@@ -787,6 +787,33 @@ namespace MobileGL::MG_State::GLState {
|
||||
void MarkUBOContentDirty() const {
|
||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||
}
|
||||
|
||||
// ---- the reserved gl_NumSamples stand-in (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME) ----
|
||||
//
|
||||
// PHASE A: answerable without joining the SPIR-V job, which is what lets the draw path ask
|
||||
// every program this question and pay nothing for the overwhelming majority that say no.
|
||||
Bool UsesReservedNumSamples() const { return Artifacts().usesReservedNumSamples; }
|
||||
|
||||
// Publishes `samples` into the global-UBO shadow. Returns false when there is nowhere to
|
||||
// put it - no shim in this program, no SPIR-V (a cancelled phase B), or the optimizer
|
||||
// dropped the member because nothing read it after all - all of which are ordinary states,
|
||||
// not errors. A value-identical write is dropped without bumping the content version, so a
|
||||
// steady stream of draws into one framebuffer does not force a re-upload per draw.
|
||||
Bool WriteReservedNumSamples(Int samples) {
|
||||
if (!UsesReservedNumSamples()) return false;
|
||||
SpirvArtifacts& spirv = Spirv();
|
||||
const Uint offset = spirv.reservedNumSamplesOffset;
|
||||
if (offset == kInvalidUniformOffset) return false;
|
||||
if (static_cast<SizeT>(offset) + sizeof(Int) > spirv.globalUboScratch.size()) return false;
|
||||
|
||||
Uint8* const slot = spirv.globalUboScratch.data() + offset;
|
||||
Int current = 0;
|
||||
Memcpy(¤t, slot, sizeof(Int));
|
||||
if (current == samples) return true;
|
||||
Memcpy(slot, &samples, sizeof(Int));
|
||||
MarkUBOContentDirty();
|
||||
return true;
|
||||
}
|
||||
// ---- glUniform* inside the phase-A -> phase-B window ----
|
||||
//
|
||||
// True while the program is fully linked and fully queryable but its uniform shadow's
|
||||
@@ -888,6 +915,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// subset of the stages of a program pipeline. Only takes effect on the next link,
|
||||
// which is why it is plain state here rather than something Link() consults.
|
||||
Bool GetSeparable() const { return m_separable; }
|
||||
// What GL_PROGRAM_SEPARABLE actually reports, and what glUseProgramStages actually
|
||||
// requires: the value the flag held at the program's LAST LINK, not the live flag.
|
||||
// GL 4.6 core 7.3 - "the flag takes effect the next time the program is linked" - so a
|
||||
// program that was told to be separable and then never linked is still NOT separable,
|
||||
// which is precisely what es31cSeparateShaderObjsTests's PipelineApi and CreateShadProgApi
|
||||
// assert. The live flag stays available as GetSeparable() for glGetProgramiv's sibling
|
||||
// state and for the next link to latch.
|
||||
Bool GetLinkedSeparable() const { return m_linkedSeparable; }
|
||||
void SetSeparable(Bool separable) {
|
||||
m_separable = separable;
|
||||
// ---- arming the uniform-write tracking latch ----
|
||||
@@ -1296,6 +1331,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
std::set<String> uniformBlocksWithoutBinding;
|
||||
|
||||
Uint activeUniformCount = 0;
|
||||
// This program's fragment stage read gl_NumSamples, so the source pipeline lowered it
|
||||
// onto the reserved default-block uniform (ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)
|
||||
// and the draw path owes it the draw framebuffer's sample count before every draw.
|
||||
//
|
||||
// PHASE A on purpose, even though the byte offset it needs is phase-B output: the
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
Uint maxUniformLocation = 0;
|
||||
Int uniformNameMaxLength = 0;
|
||||
Int attribInNameMaxLength = 0;
|
||||
@@ -1317,6 +1360,27 @@ namespace MobileGL::MG_State::GLState {
|
||||
Vector<Uint32> gsStripTriangles;
|
||||
Bool gsStripCaptureFixup = false;
|
||||
GLenum gsInputPrimitive = GL_NONE;
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES: the `layout(vertices = N) out` of the linked
|
||||
// tessellation control stage, or 0 when the program has none. Checked against
|
||||
// GL_MAX_PATCH_VERTICES at link (GL 4.6 core 11.2.1.1).
|
||||
Int tcsOutputVertices = 0;
|
||||
// The rest of the geometry stage's link properties, and the tessellation evaluation
|
||||
// stage's. Every one of these is a glGetProgramiv answer that had no source at all:
|
||||
// the query surface listed the geometry pnames only to fall through to
|
||||
// GL_INVALID_ENUM, and the GL_TESS_GEN_* pnames were not mentioned anywhere. They
|
||||
// come from the linked intermediates for the same reason gsInputPrimitive and
|
||||
// tcsOutputVertices do - glslang has already merged the compilation units' layout
|
||||
// qualifiers and diagnosed contradictions, so the linked program is the thing that
|
||||
// knows.
|
||||
GLenum gsOutputPrimitive = GL_NONE;
|
||||
Int gsMaxVertices = 0;
|
||||
Int gsInvocations = 0;
|
||||
// The tessellation evaluation stage's layout: GL_QUADS / GL_TRIANGLES / GL_ISOLINES,
|
||||
// GL_EQUAL / GL_FRACTIONAL_EVEN / GL_FRACTIONAL_ODD, GL_CW / GL_CCW, and point mode.
|
||||
GLenum tessGenMode = GL_NONE;
|
||||
GLenum tessGenSpacing = GL_NONE;
|
||||
GLenum tessGenVertexOrder = GL_NONE;
|
||||
Bool tessGenPointMode = false;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
Int xfbVaryingNameMaxLength = 0;
|
||||
Bool xfbNeedsScatteredCapture = false;
|
||||
@@ -1344,6 +1408,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||
Vector<Uint> uniformOffsets;
|
||||
Vector<Uint8> globalUboScratch;
|
||||
// Byte offset of the reserved gl_NumSamples stand-in inside globalUboScratch, or
|
||||
// kInvalidUniformOffset. Taken by NAME from the SPIR-V metadata rather than through
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
@@ -1467,6 +1536,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_requestedXfbVaryings = Move(names);
|
||||
m_requestedXfbBufferMode = bufferMode;
|
||||
}
|
||||
// NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
|
||||
// needs the capture list of the stage program it flattens, and the obvious source - what
|
||||
// glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
|
||||
// effect until the stage program's next link, and it bumps no version, so reading it makes
|
||||
// the composite's capture list depend on when the composite cache happened to be
|
||||
// invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
|
||||
// AND cache-safe, because linked state only moves at a link and the composite signature
|
||||
// already keys on the link version. See GLContext::GetProgramForDraw.
|
||||
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
|
||||
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
|
||||
@@ -1501,6 +1578,22 @@ namespace MobileGL::MG_State::GLState {
|
||||
// GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the
|
||||
// program has no geometry stage. Draws must present a compatible primitive type.
|
||||
GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; }
|
||||
// GL_GEOMETRY_OUTPUT_TYPE (GL_POINTS, GL_LINE_STRIP or GL_TRIANGLE_STRIP),
|
||||
// GL_GEOMETRY_VERTICES_OUT and GL_GEOMETRY_SHADER_INVOCATIONS of the linked geometry
|
||||
// stage. Meaningless without one - glGetProgramiv raises INVALID_OPERATION there.
|
||||
GLenum GetGeometryOutputType() const { return Artifacts().gsOutputPrimitive; }
|
||||
Int GetGeometryVerticesOut() const { return Artifacts().gsMaxVertices; }
|
||||
Int GetGeometryShaderInvocations() const { return Artifacts().gsInvocations; }
|
||||
// GL_TESS_CONTROL_OUTPUT_VERTICES of the linked tessellation control stage, or 0 when
|
||||
// the program has no such stage. Never greater than GL_MAX_PATCH_VERTICES: a program
|
||||
// that declared more does not link at all (GL 4.6 core 11.2.1.1).
|
||||
Int GetTessControlOutputVertices() const { return Artifacts().tcsOutputVertices; }
|
||||
// GL_TESS_GEN_MODE / _SPACING / _VERTEX_ORDER / _POINT_MODE of the linked tessellation
|
||||
// evaluation stage.
|
||||
GLenum GetTessGenMode() const { return Artifacts().tessGenMode; }
|
||||
GLenum GetTessGenSpacing() const { return Artifacts().tessGenSpacing; }
|
||||
GLenum GetTessGenVertexOrder() const { return Artifacts().tessGenVertexOrder; }
|
||||
Bool GetTessGenPointMode() const { return Artifacts().tessGenPointMode; }
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
|
||||
@@ -1626,6 +1719,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool m_deleteStatus = false;
|
||||
Bool m_binaryRetrievableHint = false;
|
||||
Bool m_separable = false;
|
||||
// m_separable as of the last link; see GetLinkedSeparable. Latched by Link() rather than
|
||||
// carried in LinkArtifacts because it is a GL-thread-owned decision made at enqueue time,
|
||||
// not a result the worker computes - and because a FAILED link still latches it, exactly
|
||||
// as a successful one does.
|
||||
Bool m_linkedSeparable = false;
|
||||
// Monotone "this program may ever be a pipeline stage" latch; see SetSeparable for why
|
||||
// it is a latch and not just m_separable. Outside LinkArtifacts on purpose: a relink
|
||||
// clears the write SET, but a program that was separable is still separable after it.
|
||||
|
||||
@@ -278,6 +278,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
artifacts.reservedNumSamplesOffset = ProgramObject::kInvalidUniformOffset;
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
@@ -311,6 +312,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.globalUboScratch.resize(size);
|
||||
}
|
||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||
// The gl_NumSamples stand-in is routed by NAME and nothing else. It has no GL
|
||||
// location to look up - DoReflection hides it from the GL uniform index space
|
||||
// precisely so no application can address it - so the lookup below would find
|
||||
// nothing and log it as unbacked. Only the fragment stage declares it, and
|
||||
// every stage's copy sits at the same offset in the one shared global UBO.
|
||||
if (name == NUM_SAMPLES_UNIFORM_NAME) {
|
||||
artifacts.reservedNumSamplesOffset = offset;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - reserved gl_NumSamples stand-in '%s' "
|
||||
"backed at UBO offset %u",
|
||||
externalIndex, name.c_str(), offset);
|
||||
continue;
|
||||
}
|
||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||
// suffix before declaring the uniform unbacked.
|
||||
|
||||
@@ -140,17 +140,21 @@ namespace {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
|
||||
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
|
||||
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
|
||||
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
|
||||
// number the application was told, not against either half of it.
|
||||
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers. Derived by the shared
|
||||
// ResolveResourceBindingLimits so the compile-time scan below and the link-time general check
|
||||
// (TMglGlslIoResolver::CheckDeclaredBindingRange) can never disagree about the number.
|
||||
//
|
||||
// Why BOTH still exist. GLSL makes an over-range binding a COMPILE-time error, and this scan
|
||||
// is the only place MobileGL can raise one - glslang's own ceilings are switched off by the
|
||||
// relaxed Vulkan parse and cannot be turned back on without changing the parse everything
|
||||
// else depends on. The link-time check covers the four kinds a lexical scan of unexpanded
|
||||
// source cannot see at all (samplers, images, uniform blocks, atomic counters, whose binding
|
||||
// only survives inside a synthesized block NAME) and re-covers storage blocks as a backstop.
|
||||
// The conformance predicate is compile AND link, so either site satisfies it; the split is
|
||||
// about WHICH error GL reports, not about whether the shader is rejected.
|
||||
static MobileGL::Int MaxShaderStorageBufferBindings(
|
||||
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
const MobileGL::Int frontendPoints =
|
||||
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
|
||||
if (!env.HasBackend()) return frontendPoints;
|
||||
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
|
||||
return MobileGL::MG_State::GLState::ResolveResourceBindingLimits(env).MaxShaderStorageBufferBindings;
|
||||
}
|
||||
|
||||
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||
|
||||
@@ -10,9 +10,54 @@
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Async/JobNode.h>
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
// THE one derivation of the binding ceilings a shader-declared layout(binding = N) is judged
|
||||
// against. Two readers have to agree on them - the compile-time storage-block scan below and
|
||||
// the link-time general check in TMglGlslIoResolver - and the numbers are recomputed here
|
||||
// rather than queried because both readers run on a worker with no context.
|
||||
//
|
||||
// Each is exactly what glGetIntegerv answers for the matching pname, and none of them is a
|
||||
// plain backend parameter: the buffer families are additionally capped by the state layer's
|
||||
// indexed-binding array (GL_Getter's GetIndexedBufferQueryPointCount does the same), because
|
||||
// a shader must be judged against the number the APPLICATION was told, not against either
|
||||
// half of it. Lives in MG_State rather than in MG_Util/ShaderTranspiler/Types.h purely
|
||||
// because BufferBindingPointCount is state-layer knowledge that the transpiler layer must
|
||||
// not reach up for.
|
||||
inline MG_Util::ShaderTranspiler::ResourceBindingLimits ResolveResourceBindingLimits(
|
||||
const MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||
namespace ST = MG_Util::ShaderTranspiler;
|
||||
ST::ResourceBindingLimits limits;
|
||||
const Int bindingPoints = static_cast<Int>(BufferBindingPointCount);
|
||||
// The atomic-counter ceiling is a frontend constant, so it holds even with no backend -
|
||||
// and it is the number BuildTBuiltInResource compiles a layout(binding = N) atomic_uint
|
||||
// against, which is what makes it enforceable at all.
|
||||
limits.MaxAtomicCounterBufferBindings = std::min<Int>(bindingPoints, ST::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
|
||||
// So is the uniform-buffer one: GL_MAX_UNIFORM_BUFFER_BINDINGS is clamped to the indexed
|
||||
// binding array in the getter and its floor (the GL 4.5 core minimum of 84) is that same
|
||||
// array's width, so the backend's own number never moves it.
|
||||
limits.MaxUniformBufferBindings = bindingPoints;
|
||||
// The storage-buffer ceiling has the same shape as GetIndexedBufferQueryPointCount's: the
|
||||
// backend's count capped by the array, and the array alone when there is no backend. That
|
||||
// "no backend" arm is not a detail - it is what the GPU-free test binary runs under, and
|
||||
// it has to keep matching what glGetIntegerv answers there.
|
||||
limits.MaxShaderStorageBufferBindings =
|
||||
env.HasBackend()
|
||||
? std::min<Int>(bindingPoints, std::max<Int>(env.params.MaxShaderStorageBufferBindings, 0))
|
||||
: bindingPoints;
|
||||
if (!env.HasBackend()) {
|
||||
// The two genuinely per-DEVICE ceilings have nothing to be measured against here, and
|
||||
// zero means "do not enforce this kind" rather than "reject everything".
|
||||
return limits;
|
||||
}
|
||||
limits.MaxSamplerBindings = std::max<Int>(env.params.MaxCombinedTextureImageUnits, 0);
|
||||
limits.MaxImageBindings = std::max<Int>(env.params.MaxImageUnits, 0);
|
||||
return limits;
|
||||
}
|
||||
|
||||
// glslang has no "detach this thread" API in the vendored revision, but TShader::parse
|
||||
// leaves the calling thread's TLS pool allocator pointing at the shader's own pool and
|
||||
// never restores it. Left there, the next allocation this thread makes - in an unrelated
|
||||
|
||||
@@ -15,7 +15,77 @@
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
void ShaderObject::SetSpirvBinary(Vector<Uint32>&& binary) {
|
||||
// A module replaces whatever this object stood for, so the compiled state of the old
|
||||
// source goes with it - including a compile still in flight.
|
||||
ReleaseCompileNode();
|
||||
m_spirvBinary = Move(binary);
|
||||
m_hasSpirvBinary = true;
|
||||
m_specialized = false;
|
||||
m_specializationFailed = false;
|
||||
m_specializationInfoLog.clear();
|
||||
m_spirvXfbVaryings.clear();
|
||||
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
m_source = MakeShared<const String>(String{});
|
||||
InvalidateCompiledState();
|
||||
}
|
||||
|
||||
const String& ShaderObject::GetApplicationShaderSource() const {
|
||||
static const String kNoSource;
|
||||
// Both the unspecialized and the specialized windows answer empty: in the first m_source
|
||||
// already is empty, in the second it holds generated GLSL that the application never wrote.
|
||||
return m_hasSpirvBinary ? kNoSource : *m_source;
|
||||
}
|
||||
|
||||
void ShaderObject::SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode) {
|
||||
ReleaseCompileNode();
|
||||
// The latch goes up HERE and nowhere else - this is the one path that actually specialized
|
||||
// the shader.
|
||||
m_specialized = true;
|
||||
m_specializationFailed = false;
|
||||
m_specializationInfoLog.clear();
|
||||
m_spirvXfbVaryings = Move(xfbVaryings);
|
||||
m_spirvXfbBufferMode = xfbBufferMode;
|
||||
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
|
||||
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know
|
||||
// the source was not written by the application - which is the whole reason this hop
|
||||
// exists, and the reason a SPIR-V program's GL-visible surface (uniform locations, block
|
||||
// indices, transform-feedback layout) is populated at all.
|
||||
m_source = MakeShared<const String>(Move(glsl));
|
||||
InvalidateCompiledState();
|
||||
Compile();
|
||||
}
|
||||
|
||||
void ShaderObject::RecordSpecializationFailure(String&& infoLog) {
|
||||
ReleaseCompileNode();
|
||||
m_source = MakeShared<const String>(String{});
|
||||
InvalidateCompiledState();
|
||||
m_specializationFailed = true;
|
||||
m_specializationInfoLog = Move(infoLog);
|
||||
}
|
||||
|
||||
void ShaderObject::SetShaderSource(const String& source) {
|
||||
// glShaderSource on a SPIR-V shader takes the object back to being a GLSL one, and
|
||||
// GL_SPIR_V_BINARY must then read FALSE (ARB_gl_spirv; gl4cGlSpirvTests'
|
||||
// spirv_modules_state_queries_test checks exactly this transition). The stored module goes
|
||||
// with the flag - re-specializing it would be re-specializing a shader the application has
|
||||
// already replaced. The memo below is skipped on purpose: the source may well be
|
||||
// byte-identical to the empty string this object has been holding, and keeping the
|
||||
// "compiled state" of that would keep the module's verdict too.
|
||||
if (m_hasSpirvBinary || m_specializationFailed) {
|
||||
m_hasSpirvBinary = false;
|
||||
m_spirvBinary.clear();
|
||||
m_spirvBinary.shrink_to_fit();
|
||||
m_specialized = false;
|
||||
m_specializationFailed = false;
|
||||
m_specializationInfoLog.clear();
|
||||
m_spirvXfbVaryings.clear();
|
||||
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
ReleaseCompileNode();
|
||||
m_source = MakeShared<const String>(source);
|
||||
InvalidateCompiledState();
|
||||
return;
|
||||
}
|
||||
// P0b layer 1. glShaderSource always REPLACES the source, but replacing it with a
|
||||
// byte-identical one cannot change what a compile would produce: the whole
|
||||
// pipeline (preprocess -> lexical checks -> glslang parse) is a pure function of
|
||||
@@ -36,6 +106,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void ShaderObject::SetShaderSource(String&& source) {
|
||||
if (m_hasSpirvBinary || m_specializationFailed) {
|
||||
SetShaderSource(static_cast<const String&>(source));
|
||||
return;
|
||||
}
|
||||
if (SourceMatchesCompiledState(source)) return;
|
||||
ReleaseCompileNode();
|
||||
m_source = MakeShared<const String>(Move(source));
|
||||
|
||||
@@ -65,6 +65,51 @@ namespace MobileGL {
|
||||
void SetShaderSource(const String& source);
|
||||
void SetShaderSource(String&& source);
|
||||
void Compile();
|
||||
|
||||
// ---- GL_ARB_gl_spirv ----
|
||||
// glShaderBinary(GL_SHADER_BINARY_FORMAT_SPIR_V): the object stops standing for a
|
||||
// GLSL source and starts standing for an application-supplied SPIR-V module. The
|
||||
// module is held verbatim until glSpecializeShader names an entry point for it -
|
||||
// ARB_gl_spirv makes the pair a two-step operation, and glCompileShader in between is
|
||||
// INVALID_OPERATION rather than a compile of anything.
|
||||
//
|
||||
// Both directions clear the other: glShaderSource on a SPIR-V shader takes it back to
|
||||
// being a GLSL shader with GL_SPIR_V_BINARY reading FALSE, which the conformance suite
|
||||
// checks explicitly.
|
||||
void SetSpirvBinary(Vector<Uint32>&& binary);
|
||||
Bool HasSpirvBinary() const { return m_hasSpirvBinary; }
|
||||
// ARB_gl_spirv: "Once specialized, a shader may not be re-specialized without first
|
||||
// re-associating the original SPIR-V module with it, through ShaderBinary." A second
|
||||
// glSpecializeShader is GL_INVALID_OPERATION, and this latch is what answers that.
|
||||
//
|
||||
// Set ONLY on the success path. A specialization that FAILED did not specialize the
|
||||
// shader, and the conformance suite relies on that distinction: it deliberately fails
|
||||
// specialization (a bad entry point, then an unknown constant id) on one shader object
|
||||
// and then requires the next, well-formed call on that same object to be accepted.
|
||||
Bool HasBeenSpecialized() const { return m_specialized; }
|
||||
const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
|
||||
// glSpecializeShader's half: hand the object the GLSL its module specializes to and
|
||||
// let the ordinary pipeline compile it.
|
||||
void SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode);
|
||||
// The capture the object's SPIR-V module DECLARED, as the equivalent
|
||||
// glTransformFeedbackVaryings request. Empty for a GLSL shader and for a SPIR-V module
|
||||
// that declares no transform feedback. ProgramObject::Link picks this up from the
|
||||
// program's last vertex-processing stage, because ARB_gl_spirv makes decorations the
|
||||
// only declaration form for a SPIR-V program and glTransformFeedbackVaryings has no
|
||||
// effect on one.
|
||||
const Vector<String>& GetSpirvXfbVaryings() const { return m_spirvXfbVaryings; }
|
||||
GLenum GetSpirvXfbBufferMode() const { return m_spirvXfbBufferMode; }
|
||||
// What glGetShaderSource / GL_SHADER_SOURCE_LENGTH must answer. A shader created from
|
||||
// glShaderBinary never had glShaderSource called on it, so GL 4.6 core 7.1 makes its
|
||||
// source the empty string - even after glSpecializeShader, when m_source holds the
|
||||
// SPIRV-Cross GLSL the module was translated into. That text is MobileGL's, not the
|
||||
// application's, and handing it back invites an application to cache and re-submit it.
|
||||
const String& GetApplicationShaderSource() const;
|
||||
// The other half: specialization itself failed (a bad entry point, a constant id the
|
||||
// module does not declare, a module spirv-val rejects). There is nothing to compile,
|
||||
// so the verdict is recorded directly - COMPILE_STATUS false with this log - and both
|
||||
// queries answer from it without touching the compile pipeline.
|
||||
void RecordSpecializationFailure(String&& infoLog);
|
||||
// Gives up this object's claim on its compile node, cancelling the node only if
|
||||
// this object was its LAST claimant. Called at the points where the object's
|
||||
// compiled state stops being observable through THIS name: a real source change,
|
||||
@@ -99,14 +144,16 @@ namespace MobileGL {
|
||||
const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; }
|
||||
|
||||
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
|
||||
const String& GetInfoLog() const { return Compiled().infoLog; }
|
||||
const String& GetInfoLog() const {
|
||||
return m_specializationFailed ? m_specializationInfoLog : Compiled().infoLog;
|
||||
}
|
||||
// Explicit layout(location = N) qualifiers on this shader's default-block
|
||||
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
|
||||
// discarded them (see CollectExplicitUniformLocations).
|
||||
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
|
||||
return Compiled().explicitUniformLocations;
|
||||
}
|
||||
Bool GetCompileStatus() const { return Compiled().compileStatus; }
|
||||
Bool GetCompileStatus() const { return m_specializationFailed ? false : Compiled().compileStatus; }
|
||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||
|
||||
// Blocks until a pending compile has published its artifacts. Public for the
|
||||
@@ -248,6 +295,25 @@ namespace MobileGL {
|
||||
// query optimistically for the current node. Cleared wherever the node
|
||||
// changes hands (AdoptCompileNode) or goes away (DropCompileNode).
|
||||
mutable Bool m_optimisticAnswerLatched = false;
|
||||
// The application-supplied SPIR-V module and the flag GL_SPIR_V_BINARY reports. The
|
||||
// module is kept after specialization too: glSpecializeShader may legally run again on
|
||||
// the same object with different constants, and the second call has to re-specialize
|
||||
// the ORIGINAL words rather than the ones the first call folded.
|
||||
Vector<Uint32> m_spirvBinary;
|
||||
Bool m_hasSpirvBinary = false;
|
||||
// "This shader has been specialized"; see HasBeenSpecialized. Cleared by anything that
|
||||
// re-associates a module (SetSpirvBinary) or turns the object back into a GLSL shader
|
||||
// (either SetShaderSource overload) - which is exactly the re-association ARB_gl_spirv
|
||||
// names as the way to make a second specialization legal again.
|
||||
Bool m_specialized = false;
|
||||
Vector<String> m_spirvXfbVaryings;
|
||||
GLenum m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
// A specialization that failed before any compile could start. Kept beside the
|
||||
// compile artifacts rather than inside them because there is no compile job to hang
|
||||
// it on - see RecordSpecializationFailure. Cleared by anything that gives the object
|
||||
// a new meaning (a new source, a new module, a fresh specialization).
|
||||
Bool m_specializationFailed = false;
|
||||
String m_specializationInfoLog;
|
||||
};
|
||||
} // namespace MG_State::GLState
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -217,12 +217,43 @@ namespace MobileGL {
|
||||
return m_parameters.PatchVertices;
|
||||
}
|
||||
|
||||
void RenderState::SetPolygonOffset(Float factor, Float units) {
|
||||
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
|
||||
// BumpVersions(), not just ++m_version, for the same reason SetPatchVertices does it:
|
||||
// these levels are compiled INTO the synthesized pass-through tessellation control
|
||||
// stage on both backends, so changing one makes an already-built program stale.
|
||||
//
|
||||
// The redundant-write guard compares BIT PATTERNS, not floats: glPatchParameterfv
|
||||
// accepts NaN, and a float compare would let a re-set of the identical NaN tuple fall
|
||||
// through and bump the pipeline-state version - invalidating DirectVulkan's pipeline
|
||||
// memo and DirectGLES's render-state span - on every single call.
|
||||
void RenderState::SetPatchDefaultOuterLevel(const FloatVec4& levels) {
|
||||
if (BitwiseEqual(m_parameters.PatchDefaultOuterLevel, levels)) return;
|
||||
|
||||
m_parameters.PolygonOffsetFactor = factor;
|
||||
m_parameters.PolygonOffsetUnits = units;
|
||||
++m_version;
|
||||
m_parameters.PatchDefaultOuterLevel = levels;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
const FloatVec4& RenderState::GetPatchDefaultOuterLevel() const {
|
||||
return m_parameters.PatchDefaultOuterLevel;
|
||||
}
|
||||
|
||||
void RenderState::SetPatchDefaultInnerLevel(const FloatVec2& levels) {
|
||||
if (BitwiseEqual(m_parameters.PatchDefaultInnerLevel, levels)) return;
|
||||
|
||||
m_parameters.PatchDefaultInnerLevel = levels;
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetPatchDefaultInnerLevel() const {
|
||||
return m_parameters.PatchDefaultInnerLevel;
|
||||
}
|
||||
|
||||
void RenderState::SetPolygonOffset(Float factor, Float units) {
|
||||
// GL 4.6 core 14.6.5 defines PolygonOffset(factor, units) as EQUIVALENT to
|
||||
// PolygonOffsetClamp(factor, units, 0) - the equivalence is total, so the clamp is
|
||||
// written too, not merely left alone. Leaving it meant a glPolygonOffsetClamp(1, 1,
|
||||
// 0.5) followed by a plain glPolygonOffset(3, 4) still reported a clamp of 0.5, and
|
||||
// the early-out below could even skip the version bump while doing it.
|
||||
SetPolygonOffsetClamped(factor, units, 0.0f);
|
||||
}
|
||||
|
||||
Float RenderState::GetPolygonOffsetFactor() const {
|
||||
@@ -233,6 +264,37 @@ namespace MobileGL {
|
||||
return m_parameters.PolygonOffsetUnits;
|
||||
}
|
||||
|
||||
void RenderState::SetPolygonOffsetClamped(Float factor, Float units, Float clamp) {
|
||||
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units &&
|
||||
m_parameters.PolygonOffsetClamp == clamp)
|
||||
return;
|
||||
|
||||
m_parameters.PolygonOffsetFactor = factor;
|
||||
m_parameters.PolygonOffsetUnits = units;
|
||||
m_parameters.PolygonOffsetClamp = clamp;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
Float RenderState::GetPolygonOffsetClamp() const {
|
||||
return m_parameters.PolygonOffsetClamp;
|
||||
}
|
||||
|
||||
void RenderState::SetClipControl(GLenum origin, GLenum depth) {
|
||||
if (m_parameters.ClipOrigin == origin && m_parameters.ClipDepthMode == depth) return;
|
||||
|
||||
m_parameters.ClipOrigin = origin;
|
||||
m_parameters.ClipDepthMode = depth;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
GLenum RenderState::GetClipOrigin() const {
|
||||
return m_parameters.ClipOrigin;
|
||||
}
|
||||
|
||||
GLenum RenderState::GetClipDepthMode() const {
|
||||
return m_parameters.ClipDepthMode;
|
||||
}
|
||||
|
||||
// -------------------- Capabilities --------------------
|
||||
namespace {
|
||||
// CapabilityInput lists ClipDistance0..7 contiguously (RenderState.h); the caller
|
||||
@@ -270,6 +332,7 @@ namespace MobileGL {
|
||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||
SET_CAPABILITY(SampleCoverage, enabled);
|
||||
SET_CAPABILITY(SampleMask, enabled);
|
||||
SET_CAPABILITY(SampleShading, enabled);
|
||||
SET_CAPABILITY(StencilTest, enabled);
|
||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||
case CapabilityInput::Blend: {
|
||||
@@ -344,6 +407,7 @@ namespace MobileGL {
|
||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||
RETURN_CAPABILITY(SampleCoverage);
|
||||
RETURN_CAPABILITY(SampleMask);
|
||||
RETURN_CAPABILITY(SampleShading);
|
||||
RETURN_CAPABILITY(StencilTest);
|
||||
RETURN_CAPABILITY(ProgramPointSize);
|
||||
case CapabilityInput::Blend:
|
||||
@@ -737,6 +801,20 @@ namespace MobileGL {
|
||||
return m_parameters.SampleMaskValue;
|
||||
}
|
||||
|
||||
void RenderState::SetMinSampleShadingValue(Float value) {
|
||||
if (m_parameters.MinSampleShadingValue == value) return;
|
||||
|
||||
m_parameters.MinSampleShadingValue = value;
|
||||
// BumpVersions, not just ++m_version: DirectVulkan bakes the fraction into
|
||||
// VkPipelineMultisampleStateCreateInfo::minSampleShading, so a cached pipeline
|
||||
// built with the old value must not be reused.
|
||||
BumpVersions();
|
||||
}
|
||||
|
||||
Float RenderState::GetMinSampleShadingValue() const {
|
||||
return m_parameters.MinSampleShadingValue;
|
||||
}
|
||||
|
||||
// -------------------- Pixel Store --------------------
|
||||
void RenderState::SetPixelStoreParam(PixelStoreParam param, Int value) {
|
||||
#define SET_PIXEL_STORE_PARAM(paramNameHead, paramNameTail, val) \
|
||||
|
||||
@@ -240,8 +240,24 @@ namespace MobileGL {
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
Uint PatchVertices = 3;
|
||||
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL (glPatchParameterfv). The
|
||||
// tessellation levels used when a program has an evaluation stage and NO control stage -
|
||||
// GL's fixed-function pass-through (4.6 core 11.2.2). Both backends have to synthesize
|
||||
// that stage, and they bake these numbers into it, so a change here makes an already-built
|
||||
// one stale exactly as PATCH_VERTICES does. Default 1.0, per table 23.44.
|
||||
FloatVec4 PatchDefaultOuterLevel = FloatVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
FloatVec2 PatchDefaultInnerLevel = FloatVec2(1.0f, 1.0f);
|
||||
Float PolygonOffsetFactor = 0.0f;
|
||||
Float PolygonOffsetUnits = 0.0f;
|
||||
// GL_POLYGON_OFFSET_CLAMP (GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp): the maximum
|
||||
// magnitude of the offset glPolygonOffsetClamp's third argument allows. Zero - the default
|
||||
// - means "no clamp", which is exactly the behaviour glPolygonOffset leaves behind.
|
||||
Float PolygonOffsetClamp = 0.0f;
|
||||
|
||||
// glClipControl (GL 4.5 core 13.5). Defaults per table 23.7 are the pre-4.5 fixed
|
||||
// behaviour: origin at the lower left, depth mapped from -1..1.
|
||||
GLenum ClipOrigin = GL_LOWER_LEFT;
|
||||
GLenum ClipDepthMode = GL_NEGATIVE_ONE_TO_ONE;
|
||||
|
||||
// Blending
|
||||
Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS> BlendStates;
|
||||
@@ -271,6 +287,10 @@ namespace MobileGL {
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
// glMinSampleShading (ARB_sample_shading / GL 4.0 core 14.3.1). The fraction of samples
|
||||
// that get their own independent shading when GL_SAMPLE_SHADING is enabled; the initial
|
||||
// value is 0, and the value is clamped to [0, 1] on the way in.
|
||||
Float MinSampleShadingValue = 0.0f;
|
||||
Array<StencilFaceState, 2> StencilStates{};
|
||||
|
||||
// Cull Face
|
||||
@@ -319,6 +339,7 @@ namespace MobileGL {
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool SampleShadingEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
@@ -374,9 +395,21 @@ namespace MobileGL {
|
||||
Float GetPointSize() const;
|
||||
void SetPatchVertices(Uint vertices);
|
||||
Uint GetPatchVertices() const;
|
||||
void SetPatchDefaultOuterLevel(const FloatVec4& levels);
|
||||
const FloatVec4& GetPatchDefaultOuterLevel() const;
|
||||
void SetPatchDefaultInnerLevel(const FloatVec2& levels);
|
||||
const FloatVec2& GetPatchDefaultInnerLevel() const;
|
||||
void SetPolygonOffset(Float factor, Float units);
|
||||
// glPolygonOffsetClamp. Writes the same factor/units as glPolygonOffset plus the
|
||||
// clamp, because that is what the entry point does - glPolygonOffset is the
|
||||
// clamp = 0 case of it (GL 4.6 core 14.6.5).
|
||||
void SetPolygonOffsetClamped(Float factor, Float units, Float clamp);
|
||||
Float GetPolygonOffsetFactor() const;
|
||||
Float GetPolygonOffsetUnits() const;
|
||||
Float GetPolygonOffsetClamp() const;
|
||||
void SetClipControl(GLenum origin, GLenum depth);
|
||||
GLenum GetClipOrigin() const;
|
||||
GLenum GetClipDepthMode() const;
|
||||
// Hints. target must be one of the 4 GL 3.3 core hint targets (validated by the caller).
|
||||
void SetHint(GLenum target, GLenum mode);
|
||||
GLenum GetHint(GLenum target) const;
|
||||
@@ -454,6 +487,9 @@ namespace MobileGL {
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
void SetSampleMaskValue(Uint32 mask);
|
||||
Uint32 GetSampleMaskValue() const;
|
||||
// glMinSampleShading. `value` is stored as given; the entry point clamps.
|
||||
void SetMinSampleShadingValue(Float value);
|
||||
Float GetMinSampleShadingValue() const;
|
||||
|
||||
// Pixel Store
|
||||
void SetPixelStoreParam(PixelStoreParam param, Int value);
|
||||
|
||||
@@ -155,9 +155,21 @@ namespace MobileGL {
|
||||
// an answer whichever form was written. Integer <-> float uses the plain value, matching
|
||||
// what glTexParameterIiv/Iuiv mean: those forms are for integer texture formats, whose
|
||||
// border components are the raw integers rather than a normalized fraction.
|
||||
//
|
||||
// Which of the three the application actually WROTE is recorded separately in
|
||||
// borderColorForm, because the derived values erase it: a backend has to know whether to
|
||||
// forward the colour through glSamplerParameterfv or glSamplerParameterIiv (and which
|
||||
// VkBorderColor family to ask Vulkan for), and the numbers alone cannot say. That is also
|
||||
// why every setter's early-out tests the form as well as the value - a float (0,0,0,1)
|
||||
// followed by an integer (0,0,0,1) is a real state change even though nothing numeric
|
||||
// moved, and swallowing it would leave the backend syncing the wrong entry point forever.
|
||||
void SamplerObject::SetBorderColor(const FloatVec4& color) {
|
||||
if (color == m_samplerParameters.borderColor) return;
|
||||
if (color == m_samplerParameters.borderColor &&
|
||||
m_samplerParameters.borderColorForm == BorderColorForm::Float) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_samplerParameters.borderColorForm = BorderColorForm::Float;
|
||||
m_samplerParameters.borderColor = color;
|
||||
m_samplerParameters.borderColorI =
|
||||
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
|
||||
@@ -169,8 +181,12 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void SamplerObject::SetBorderColorI(const IntVec4& color) {
|
||||
if (color == m_samplerParameters.borderColorI) return;
|
||||
if (color == m_samplerParameters.borderColorI &&
|
||||
m_samplerParameters.borderColorForm == BorderColorForm::Int) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_samplerParameters.borderColorForm = BorderColorForm::Int;
|
||||
m_samplerParameters.borderColorI = color;
|
||||
m_samplerParameters.borderColorUI =
|
||||
UintVec4(static_cast<Uint32>(color.x()), static_cast<Uint32>(color.y()),
|
||||
@@ -182,8 +198,12 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void SamplerObject::SetBorderColorUI(const UintVec4& color) {
|
||||
if (color == m_samplerParameters.borderColorUI) return;
|
||||
if (color == m_samplerParameters.borderColorUI &&
|
||||
m_samplerParameters.borderColorForm == BorderColorForm::Uint) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_samplerParameters.borderColorForm = BorderColorForm::Uint;
|
||||
m_samplerParameters.borderColorUI = color;
|
||||
m_samplerParameters.borderColorI =
|
||||
IntVec4(static_cast<Int32>(color.x()), static_cast<Int32>(color.y()),
|
||||
@@ -206,6 +226,10 @@ namespace MobileGL {
|
||||
return m_samplerParameters.borderColorUI;
|
||||
}
|
||||
|
||||
BorderColorForm SamplerObject::GetBorderColorForm() const {
|
||||
return m_samplerParameters.borderColorForm;
|
||||
}
|
||||
|
||||
SamplerCompareMode SamplerObject::GetCompareMode() const {
|
||||
return m_samplerParameters.compareMode;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,19 @@ namespace MobileGL {
|
||||
Unknown = -1
|
||||
};
|
||||
|
||||
// Which of the three GL_TEXTURE_BORDER_COLOR entry-point families last wrote the border colour,
|
||||
// and therefore which of the three stored representations is AUTHORITATIVE. GL 4.6 core 8.10:
|
||||
// TexParameterIiv/Iuiv store an integer border colour "unmodified, with an internal data type of
|
||||
// integer", TexParameterfv stores a floating-point one, and the derived forms are only a
|
||||
// convenience for a getter of the other spelling. A backend cannot pick the right driver entry
|
||||
// point (glSamplerParameterIiv vs fv) or the right VkBorderColor family without this: numerically
|
||||
// the three representations are always populated, so the value alone says nothing about the form.
|
||||
enum class BorderColorForm : Uint8 {
|
||||
Float,
|
||||
Int,
|
||||
Uint
|
||||
};
|
||||
|
||||
struct SamplerParameters {
|
||||
SamplerWrapMode wrapS = SamplerWrapMode::Repeat;
|
||||
SamplerWrapMode wrapT = SamplerWrapMode::Repeat;
|
||||
@@ -79,6 +92,7 @@ namespace MobileGL {
|
||||
FloatVec4 borderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
IntVec4 borderColorI = {0, 0, 0, 0};
|
||||
UintVec4 borderColorUI = {0, 0, 0, 0};
|
||||
BorderColorForm borderColorForm = BorderColorForm::Float;
|
||||
};
|
||||
|
||||
namespace MG_State {
|
||||
@@ -117,6 +131,7 @@ namespace MobileGL {
|
||||
const FloatVec4& GetBorderColor() const;
|
||||
const IntVec4& GetBorderColorI() const;
|
||||
const UintVec4& GetBorderColorUI() const;
|
||||
BorderColorForm GetBorderColorForm() const;
|
||||
Uint GetExternalIndex() const;
|
||||
Uint16 GetVersion() const;
|
||||
// Globally-unique, never-reused id for this sampler object's lifetime. Lets a
|
||||
|
||||
@@ -113,8 +113,14 @@ namespace MobileGL {
|
||||
return m_sampler->GetBorderColor();
|
||||
}
|
||||
|
||||
// The redundancy filters test the FORM as well as the value: the derived representations
|
||||
// make a float (0,0,0,1) and an integer (0,0,0,1) numerically identical, but they are
|
||||
// different GL state and the DirectGLES sync memoises on m_textureParamsVersion.
|
||||
void TextureObjectBase::SetBorderColor(const FloatVec4& color) {
|
||||
if (color == m_sampler->GetBorderColor()) return;
|
||||
if (color == m_sampler->GetBorderColor() &&
|
||||
m_sampler->GetBorderColorForm() == BorderColorForm::Float) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sampler->SetBorderColor(color);
|
||||
++m_textureParamsVersion;
|
||||
@@ -125,7 +131,10 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetBorderColorI(const IntVec4& color) {
|
||||
if (color == m_sampler->GetBorderColorI()) return;
|
||||
if (color == m_sampler->GetBorderColorI() &&
|
||||
m_sampler->GetBorderColorForm() == BorderColorForm::Int) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sampler->SetBorderColorI(color);
|
||||
++m_textureParamsVersion;
|
||||
@@ -136,12 +145,19 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetBorderColorUI(const UintVec4& color) {
|
||||
if (color == m_sampler->GetBorderColorUI()) return;
|
||||
if (color == m_sampler->GetBorderColorUI() &&
|
||||
m_sampler->GetBorderColorForm() == BorderColorForm::Uint) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sampler->SetBorderColorUI(color);
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
|
||||
BorderColorForm TextureObjectBase::GetBorderColorForm() const {
|
||||
return m_sampler->GetBorderColorForm();
|
||||
}
|
||||
|
||||
TextureSwizzleParam TextureObjectBase::GetSwizzleParam(TextureSwizzleParam param) const {
|
||||
switch (param) {
|
||||
case TextureSwizzleParam::Red:
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual void SetBorderColorI(const IntVec4& color) = 0;
|
||||
virtual const UintVec4& GetBorderColorUI() const = 0;
|
||||
virtual void SetBorderColorUI(const UintVec4& color) = 0;
|
||||
// Which of the three setters above last ran; see SamplerParameters::borderColorForm.
|
||||
virtual BorderColorForm GetBorderColorForm() const = 0;
|
||||
virtual TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const = 0;
|
||||
virtual void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) = 0;
|
||||
virtual void SetSwizzleParamRGBA(const Vec4<TextureSwizzleParam>& values) = 0;
|
||||
@@ -49,6 +51,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual void SetMaxLevel(Uint maxLevel) = 0;
|
||||
virtual Bool IsImmutable() const = 0;
|
||||
virtual Uint GetImmutableLevels() const = 0;
|
||||
// How many levels THIS object can address, i.e. the bound a level argument has to
|
||||
// stay under. The same number as GetImmutableLevels() for an ordinary immutable
|
||||
// texture, but NOT for a view: GL 4.6 core 8.18 defines TEXTURE_IMMUTABLE_LEVELS on a
|
||||
// view as the ORIGINAL texture's value, which says nothing about what the view itself
|
||||
// can reach, and bounding by it lets a level the view does not have through.
|
||||
virtual Uint GetAddressableLevelCount() const = 0;
|
||||
virtual void SetImmutableLevels(Uint levels) = 0;
|
||||
virtual Uint16 GetTextureParamsVersion() const = 0;
|
||||
// Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty).
|
||||
@@ -123,6 +131,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetBorderColorI(const IntVec4& color) override;
|
||||
const UintVec4& GetBorderColorUI() const override;
|
||||
void SetBorderColorUI(const UintVec4& color) override;
|
||||
BorderColorForm GetBorderColorForm() const override;
|
||||
TextureSwizzleParam GetSwizzleParam(TextureSwizzleParam param) const override;
|
||||
const Vec4<TextureSwizzleParam>& GetAllSwizzleParams() const override;
|
||||
void SetSwizzleParam(TextureSwizzleParam param, TextureSwizzleParam value) override;
|
||||
@@ -132,6 +141,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetMaxLevel(Uint maxLevel) override;
|
||||
Bool IsImmutable() const override;
|
||||
Uint GetImmutableLevels() const override;
|
||||
// m_immutableLevels is already the VIEW-relative count for a view (its constructor
|
||||
// stores <numlevels> there so the level-range clamp works in view coordinates), so
|
||||
// this one accessor is correct for both and needs no override.
|
||||
Uint GetAddressableLevelCount() const override { return m_immutableLevels; }
|
||||
void SetImmutableLevels(Uint levels) override;
|
||||
Uint16 GetTextureParamsVersion() const override;
|
||||
Uint64 GetContentVersion() const override;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
|
||||
@@ -1296,8 +1298,13 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
|
||||
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
|
||||
// ES link error against a program that has nothing else wrong with it.
|
||||
|
||||
namespace {
|
||||
const FloatVec4 kDefaultOuter(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
const FloatVec2 kDefaultInner(1.0f, 1.0f);
|
||||
} // namespace
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "");
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", kDefaultOuter, kDefaultInner);
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
|
||||
EXPECT_TRUE(Contains(out,
|
||||
@@ -1314,10 +1321,63 @@ TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel
|
||||
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
|
||||
}
|
||||
|
||||
// glPatchParameterfv's state is compiled INTO this stage: ES has no PATCH_DEFAULT_*_LEVEL and no
|
||||
// entry point to forward it to, so a generator that ignored these arguments would tessellate every
|
||||
// control-stage-less program at level 1 whatever the application asked for.
|
||||
TEST(PassthroughTessControlEsslTest, BakesTheDefaultTessLevelsIn) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 3.0f, 4.0f, 5.0f),
|
||||
FloatVec2(6.5f, 7.25f));
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 2.0;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 3.0;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[2] = 4.0;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[3] = 5.0;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 6.5;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[1] = 7.25;")) << out;
|
||||
}
|
||||
|
||||
// Every level literal carries a decimal point even when the value is integral: ESSL reads
|
||||
// `gl_TessLevelOuter[0] = 1;` as an int assigned to a float and refuses to compile the stage,
|
||||
// which would take the whole program down with it.
|
||||
TEST(PassthroughTessControlEsslTest, SpellsIntegralLevelsAsFloatLiterals) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "", FloatVec4(2.0f, 2.0f, 2.0f, 2.0f),
|
||||
FloatVec2(2.0f, 2.0f));
|
||||
EXPECT_FALSE(Contains(out, "= 2;")) << out;
|
||||
}
|
||||
|
||||
// glPatchParameterfv accepts any float, NaN and infinity included, and GL 4.6 core 11.2.2
|
||||
// discards a patch ONLY when a relevant outer level is <= 0 - everything else is clamped into
|
||||
// [1, MAX_TESS_GEN_LEVEL]. So the three non-finite inputs do not share one answer: NaN is
|
||||
// unspecified and 0.0 is the safe reading, -inf really does discard, and +inf must tessellate at
|
||||
// the maximum. Baking 0.0 for +inf inverted "as finely as possible" into "draw nothing".
|
||||
TEST(PassthroughTessControlEsslTest, NonFiniteLevelsFollowTheDiscardRule) {
|
||||
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
|
||||
const Float infinity = std::numeric_limits<Float>::infinity();
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
|
||||
FloatVec4(notANumber, -infinity, infinity, 1.0f),
|
||||
FloatVec2(notANumber, 1.0f));
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelOuter[1] = 0.0;")) << out;
|
||||
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[2] = 0.0;"))
|
||||
<< "a positive infinity clamps to GL_MAX_TESS_GEN_LEVEL, not to a discarded patch" << out;
|
||||
EXPECT_TRUE(Contains(out, "gl_TessLevelInner[0] = 0.0;")) << out;
|
||||
EXPECT_FALSE(Contains(out, "nan")) << out;
|
||||
EXPECT_FALSE(Contains(out, "inf")) << out;
|
||||
}
|
||||
|
||||
// A level below the old six-decimal format's resolution is still a POSITIVE level, which GL clamps
|
||||
// to 1 and draws; rendering it as "0.000000" discarded the patch instead.
|
||||
TEST(PassthroughTessControlEsslTest, TinyPositiveLevelsDoNotFlushToZero) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "",
|
||||
FloatVec4(1e-7f, 1.0f, 1.0f, 1.0f),
|
||||
FloatVec2(1.0f, 1.0f));
|
||||
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.0;")) << out;
|
||||
EXPECT_FALSE(Contains(out, "gl_TessLevelOuter[0] = 0.000000;")) << out;
|
||||
}
|
||||
|
||||
// ES 3.1 reaches tessellation only through the extension; the caller has already established
|
||||
// that the driver runs the evaluation stage at all, so the only question is the spelling.
|
||||
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
||||
const String out = BuildPassthroughTessControlEssl(310, 3, "", "");
|
||||
const String out = BuildPassthroughTessControlEssl(310, 3, "", "", kDefaultOuter, kDefaultInner);
|
||||
EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
|
||||
}
|
||||
@@ -1325,7 +1385,7 @@ TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
||||
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
|
||||
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
|
||||
const String outMembers = " highp vec4 gl_Position; ";
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers);
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers, kDefaultOuter, kDefaultInner);
|
||||
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
|
||||
}
|
||||
|
||||
@@ -1252,3 +1252,194 @@ TEST_F(FramebufferTest, ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffe
|
||||
EXPECT_EQ(g_driverIndexedColorMasks[2].a, GL_TRUE) << "a native buffer keeps its alpha writes";
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// --- glFramebufferTexture error conditions (GL 4.6 core 9.2.8) ---------------------------------
|
||||
//
|
||||
// Four of them were missing from the bound-target path while its DSA sibling
|
||||
// (glNamedFramebufferTexture) implemented all four, which is what KHR-GL4x.geometry_shader.
|
||||
// layered_fbo.fb_texture_* fails on. Two of them - the attachment-range check and the
|
||||
// default-framebuffer rejection - newly REFUSE calls that used to succeed, so they are pinned
|
||||
// here rather than left to the conformance suite.
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTextureRejectsTheDefaultFramebuffer) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 64, 32);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// MobileGL models framebuffer 0 as a real FramebufferObject, so the null test that used to
|
||||
// stand in for this could never fire and the attach silently "succeeded".
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTextureRejectsAColourAttachmentPastTheLimit) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 64, 32);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The same limit ValidateColorAttachmentInRange reads, so the test cannot disagree with the
|
||||
// implementation about where the boundary is.
|
||||
const GLint limit = MG_Backend::pActiveBackendObject
|
||||
? static_cast<GLint>(
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxColorAttachments)
|
||||
: static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
ASSERT_GT(limit, 0);
|
||||
ASSERT_LT(limit, 32) << "the test needs a colour attachment enum past the limit to exist";
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER,
|
||||
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit), texture, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// The last legal one still attaches, so the boundary is off-by-none.
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER,
|
||||
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit - 1), texture, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTextureReportsInvalidValueForANameThatWasNeverGenerated) {
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// INVALID_VALUE, not INVALID_OPERATION: the entry point used to resolve the texture object
|
||||
// first and report the miss with the wrong code, pre-empting ValidateTextureName.
|
||||
const GLuint neverGenerated = std::numeric_limits<GLuint>::max();
|
||||
ASSERT_FALSE(MG_State::pGLContext->ValidateTextureName(neverGenerated));
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, neverGenerated, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTextureRejectsALevelTheTextureDoesNotHave) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
// Two levels of immutable storage: level 1 is legal, level 2 is not.
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the last level the texture has is legal";
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 2);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, -1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// The four conditions above are stated once in GL 4.6 core 9.2.8 for the WHOLE family, and
|
||||
// glFramebufferTexture2D / 3D / TextureLayer reach the attachment through their own code rather
|
||||
// than through the shared helper - so each of them has to be asked separately or one entry point
|
||||
// answers differently from its aliases. glFramebufferTexture2D is the most-used of the five, and
|
||||
// the default-framebuffer case is the damaging one: the attach used to succeed and replace
|
||||
// framebuffer 0's colour attachment, which nothing ever puts back.
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTexture2DRejectsTheDefaultFramebufferAndBadAttachments) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
|
||||
ASSERT_NE(defaultFramebuffer, nullptr);
|
||||
const auto& colorBefore = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::Color0);
|
||||
const Bool hadTextureBefore = colorBefore.IsTexture();
|
||||
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
DrainPendingGlErrors();
|
||||
// ...and, more to the point, the default framebuffer still describes the surface.
|
||||
const auto& colorAfter = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::Color0);
|
||||
EXPECT_EQ(colorAfter.IsTexture(), hadTextureBefore);
|
||||
if (colorAfter.IsTexture() && hadTextureBefore) {
|
||||
EXPECT_NE(colorAfter.GetTexture()->GetExternalIndex(), texture)
|
||||
<< "the refused attach must not have replaced framebuffer 0's colour attachment";
|
||||
}
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
const GLint limit = MG_Backend::pActiveBackendObject
|
||||
? static_cast<GLint>(
|
||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxColorAttachments)
|
||||
: static_cast<GLint>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
|
||||
static_cast<GLenum>(GL_COLOR_ATTACHMENT0 + limit), GL_TEXTURE_2D,
|
||||
texture, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE) << "the texture has two levels, not three";
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, -1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// The legal call still works, so the boundary is off-by-none.
|
||||
MG_Impl::GLImpl::FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(FramebufferTest, FramebufferTextureLayerRejectsTheDefaultFramebufferAndBadLevels) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(texture, 2, GL_RGBA8, 16, 16, 4);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// The attach path used to bypass every one of these while the DETACH path (texture == 0) went
|
||||
// through the fixed helper, so one entry point answered two different ways.
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 2, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 1, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The DSA sibling is the entry point the bound-target family was aligned WITH, so an out-of-range
|
||||
// immutable level has to be rejected there too - otherwise the alignment created a fresh
|
||||
// asymmetry in the opposite direction.
|
||||
TEST_F(FramebufferTest, NamedFramebufferTextureRejectsALevelTheTextureDoesNotHave) {
|
||||
GLuint framebuffer = 0;
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 64, 32);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 1);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 2);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
@@ -101,9 +101,14 @@ namespace {
|
||||
return builtIns;
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices) {
|
||||
const FloatVec4 kDefaultOuter(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
const FloatVec2 kDefaultInner(1.0f, 1.0f);
|
||||
|
||||
Vector<Uint32> CompileGeneratedSource(Uint32 patchVertices,
|
||||
Uint32 perVertexMembers = ProgramFactory::kDefaultPerVertexMembers) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices);
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(patchVertices, kDefaultOuter,
|
||||
kDefaultInner, perVertexMembers);
|
||||
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
@@ -162,11 +167,85 @@ TEST_F(PassthroughTessControlTest, ForwardsPositionAndWritesBothLevelArrays) {
|
||||
// user-defined varying, ReflectPassthroughTessControlNeed's "built-ins only" refusal stops being
|
||||
// the right gate and both have to move together.
|
||||
TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(4);
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(
|
||||
4, kDefaultOuter, kDefaultInner, ProgramFactory::kDefaultPerVertexMembers);
|
||||
EXPECT_EQ(source.find("layout(location"), String::npos) << source;
|
||||
EXPECT_NE(source.find("layout(vertices = 4) out;"), String::npos) << source;
|
||||
}
|
||||
|
||||
// glPatchParameterfv's levels are compiled into this stage - Vulkan has no dynamic state for them -
|
||||
// so two different level sets must produce two different sources AND two different cache keys.
|
||||
// Without the second half a pipeline memoised at one set of levels would be handed back after the
|
||||
// application changed them, and the tessellation would silently stay at the old levels.
|
||||
TEST_F(PassthroughTessControlTest, BakesTheDefaultTessLevelsInAndKeysOnThem) {
|
||||
constexpr Uint32 kMembers = ProgramFactory::kDefaultPerVertexMembers;
|
||||
const FloatVec4 outer(2.0f, 3.0f, 4.0f, 5.0f);
|
||||
const FloatVec2 inner(6.5f, 7.25f);
|
||||
const String source = ProgramFactory::BuildPassthroughTessControlSource(4, outer, inner,
|
||||
ProgramFactory::kDefaultPerVertexMembers);
|
||||
EXPECT_NE(source.find("gl_TessLevelOuter[0] = 2.0;"), String::npos) << source;
|
||||
EXPECT_NE(source.find("gl_TessLevelOuter[3] = 5.0;"), String::npos) << source;
|
||||
EXPECT_NE(source.find("gl_TessLevelInner[0] = 6.5;"), String::npos) << source;
|
||||
EXPECT_NE(source.find("gl_TessLevelInner[1] = 7.25;"), String::npos) << source;
|
||||
|
||||
const Uint64 defaultKey =
|
||||
ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembers);
|
||||
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, outer, inner, kMembers), defaultKey);
|
||||
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, inner, kMembers), defaultKey);
|
||||
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(3, kDefaultOuter, kDefaultInner, kMembers), defaultKey);
|
||||
EXPECT_EQ(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembers), defaultKey);
|
||||
// ...and the gl_PerVertex member set is in the same key, for the same reason: two programs at
|
||||
// different GLSL versions need differently-shaped modules, and a pipeline memoised against one
|
||||
// shape must not be handed back for the other.
|
||||
constexpr Uint32 kMembersWithCull =
|
||||
ProgramFactory::kDefaultPerVertexMembers |
|
||||
static_cast<Uint32>(ProgramFactory::PerVertexMemberBit::CullDistance);
|
||||
EXPECT_NE(ProgramFactory::ComputePassthroughTessControlKey(4, kDefaultOuter, kDefaultInner, kMembersWithCull),
|
||||
defaultKey);
|
||||
}
|
||||
|
||||
// The generated stage still has to COMPILE with non-default levels: an integral level spelled
|
||||
// without a decimal point is an int literal, and `gl_TessLevelOuter[0] = 2;` does not compile.
|
||||
TEST_F(PassthroughTessControlTest, CompilesWithNonDefaultLevels) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
const String source =
|
||||
ProgramFactory::BuildPassthroughTessControlSource(4, FloatVec4(2.0f, 2.0f, 2.0f, 2.0f),
|
||||
FloatVec2(2.0f, 2.0f),
|
||||
ProgramFactory::kDefaultPerVertexMembers);
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log) << source;
|
||||
}
|
||||
|
||||
// The generator honours the mask it is given, in both directions. This is what covers the
|
||||
// pre-cutoff three-member form now that no authorable tessellation evaluation stage produces it
|
||||
// (ARB_tessellation_shader is GL 4.0 and gl_CullDistance joins the block at 400), and it is also
|
||||
// the fallback ReflectPassthroughTessControlNeed uses when it cannot read a module's block.
|
||||
TEST_F(PassthroughTessControlTest, RedeclaresExactlyTheRequestedMembers) {
|
||||
using Bit = ProgramFactory::PerVertexMemberBit;
|
||||
constexpr Uint32 kWithCull = ProgramFactory::kDefaultPerVertexMembers | static_cast<Uint32>(Bit::CullDistance);
|
||||
constexpr Uint32 kBuiltInCullDistance = 4;
|
||||
constexpr Uint32 kBuiltInClipDistance = 3;
|
||||
|
||||
const Vector<Uint32> withoutCull = CompileGeneratedSource(4, ProgramFactory::kDefaultPerVertexMembers);
|
||||
ASSERT_FALSE(withoutCull.empty());
|
||||
const std::set<Uint32> withoutCullBuiltIns = DeclaredBuiltIns(withoutCull);
|
||||
EXPECT_TRUE(withoutCullBuiltIns.contains(kBuiltInClipDistance));
|
||||
EXPECT_FALSE(withoutCullBuiltIns.contains(kBuiltInCullDistance))
|
||||
<< "the three-member mask must not emit gl_CullDistance";
|
||||
for (const auto& [structId, shape] : BuiltInBlockShapes(withoutCull)) {
|
||||
EXPECT_EQ(StructMemberCount(withoutCull, structId), 3u) << "structId=" << structId;
|
||||
}
|
||||
|
||||
const Vector<Uint32> withCull = CompileGeneratedSource(4, kWithCull);
|
||||
ASSERT_FALSE(withCull.empty());
|
||||
EXPECT_TRUE(DeclaredBuiltIns(withCull).contains(kBuiltInCullDistance))
|
||||
<< "the four-member mask must emit gl_CullDistance";
|
||||
for (const auto& [structId, shape] : BuiltInBlockShapes(withCull)) {
|
||||
EXPECT_EQ(StructMemberCount(withCull, structId), 4u) << "structId=" << structId;
|
||||
}
|
||||
}
|
||||
|
||||
// THE load-bearing test. Vulkan matches built-in interface blocks by their whole shape, and this
|
||||
// stage is compiled ON ITS OWN - it never goes through the glslang link that gives a real program
|
||||
// its gl_PerVertex. So the shape it declares has to equal the shape a linked vertex+evaluation
|
||||
@@ -179,13 +258,33 @@ TEST_F(PassthroughTessControlTest, InterfaceIsBuiltInsOnly) {
|
||||
TEST_F(PassthroughTessControlTest, MatchesTheFrontendPerVertexBlock) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Deliberately the shape of KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation:
|
||||
// a vertex stage feeding an evaluation stage with no control stage in between.
|
||||
static const char* kVs = R"(#version 430 core
|
||||
// MORE THAN ONE VERSION, because the shape is a function of the neighbour's GLSL version and
|
||||
// a single-version case cannot see that. glslang gates gl_PerVertex's gl_CullDistance member
|
||||
// on a version cutoff, and this case used to link #version 430 ONLY - which is exactly why a
|
||||
// generator hardcoded to the pre-cutoff three-member form looked correct while every program
|
||||
// above it, including every ESSL program (the source processor rewrites those to
|
||||
// "#version 460 core"), was silently mismatched.
|
||||
//
|
||||
// The expected member COUNT is deliberately not spelled per version any more. It moved once
|
||||
// already (the fork's GL_ARB_cull_distance work lowered the cutoff from 450 to 400, so 430
|
||||
// went from three members to four), and pinning it here only produced a test that failed for
|
||||
// being right. What must hold - and is what this case now asserts - is that the generator
|
||||
// reproduces whatever glslang produced, at every version, plus the floor that a per-vertex
|
||||
// block always has at least gl_Position. A tessellation evaluation stage cannot be authored
|
||||
// below #version 400 at all (ARB_tessellation_shader is GL 4.0), so 400 is the bottom of the
|
||||
// reachable range; the pre-cutoff three-member form is covered through the explicit-mask case
|
||||
// below instead.
|
||||
for (const char* version : {"#version 400 core", "#version 430 core", "#version 460 core"}) {
|
||||
SCOPED_TRACE(version);
|
||||
|
||||
// Deliberately the shape of
|
||||
// KHR-GL43.shader_storage_buffer_object.advanced-write-tessellation: a vertex stage
|
||||
// feeding an evaluation stage with no control stage in between.
|
||||
const String vs = String(version) + R"(
|
||||
layout(location = 0) in vec4 g_in_position;
|
||||
void main() { gl_Position = g_in_position; }
|
||||
)";
|
||||
static const char* kTes = R"(#version 430 core
|
||||
const String tes = String(version) + R"(
|
||||
layout(quads) in;
|
||||
void main() {
|
||||
vec4 p0 = mix(gl_in[0].gl_Position, gl_in[1].gl_Position, gl_TessCoord.x);
|
||||
@@ -193,41 +292,47 @@ void main() {
|
||||
gl_Position = mix(p0, p1, gl_TessCoord.y);
|
||||
}
|
||||
)";
|
||||
static const char* kFs = R"(#version 430 core
|
||||
const String fs = String(version) + R"(
|
||||
layout(location = 0) out vec4 g_fs_out;
|
||||
void main() { g_fs_out = vec4(0, 1, 0, 1); }
|
||||
)";
|
||||
|
||||
const Vector<GLenum> types{GL_VERTEX_SHADER, GL_TESS_EVALUATION_SHADER, GL_FRAGMENT_SHADER};
|
||||
const Vector<const char*> sources{kVs, kTes, kFs};
|
||||
const Vector<const String*> sources{&vs, &tes, &fs};
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
for (SizeT i = 0; i < types.size(); ++i) {
|
||||
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = sources[i]};
|
||||
ShaderAttrib attrib{.shaderType = types[i], .sourceStr = *sources[i]};
|
||||
auto compiled = ShaderCompiler::CompileShader(attrib);
|
||||
ASSERT_TRUE(compiled) << compiled.error().log;
|
||||
ASSERT_TRUE(compiled) << (compiled ? String{} : compiled.error().log);
|
||||
shaders.push_back(compiled.value());
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto linked = ShaderCompiler::LinkProgram(programAttrib);
|
||||
ASSERT_TRUE(linked) << linked.error().log;
|
||||
ASSERT_TRUE(linked) << (linked ? String{} : linked.error().log);
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *linked.value()};
|
||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
ASSERT_TRUE(binary);
|
||||
ASSERT_EQ(binary->size(), types.size());
|
||||
|
||||
// The evaluation stage's gl_in is the block the pass-through has to feed. It is the only
|
||||
// built-in block that stage declares as an input, so the module holds exactly one such shape
|
||||
// besides its own gl_PerVertex output - and both are the same shape, which is the point.
|
||||
// built-in block that stage declares as an input, so the module holds exactly one such
|
||||
// shape besides its own gl_PerVertex output - and both are the same shape, which is the
|
||||
// point.
|
||||
const auto tesShapes = BuiltInBlockShapes((*binary)[1]);
|
||||
ASSERT_FALSE(tesShapes.empty());
|
||||
const Vector<Uint32> frontendShape = tesShapes.begin()->second;
|
||||
const Uint32 frontendMembers = StructMemberCount((*binary)[1], tesShapes.begin()->first);
|
||||
EXPECT_GE(frontendMembers, 1u) << "a gl_PerVertex block always carries at least gl_Position";
|
||||
for (const auto& [structId, shape] : tesShapes) {
|
||||
EXPECT_EQ(shape, frontendShape) << "the evaluation stage's own built-in blocks disagree";
|
||||
EXPECT_EQ(StructMemberCount((*binary)[1], structId), frontendMembers);
|
||||
}
|
||||
|
||||
const Vector<Uint32> passthrough = CompileGeneratedSource(4);
|
||||
// ...and the generator is driven the way PRODUCTION drives it: the mask comes from the
|
||||
// evaluation stage's own module, not from a constant the test picked.
|
||||
const Uint32 reflectedMembers = ProgramFactory::ReflectPerVertexInputMembers((*binary)[1]);
|
||||
EXPECT_NE(reflectedMembers, 0u) << "the input per-vertex block walk found nothing to match against";
|
||||
const Vector<Uint32> passthrough = CompileGeneratedSource(4, reflectedMembers);
|
||||
ASSERT_FALSE(passthrough.empty());
|
||||
const auto passthroughShapes = BuiltInBlockShapes(passthrough);
|
||||
ASSERT_FALSE(passthroughShapes.empty());
|
||||
@@ -245,3 +350,4 @@ void main() { g_fs_out = vec4(0, 1, 0, 1); }
|
||||
}
|
||||
EXPECT_EQ(perVertexBlocksChecked, 2u) << "expected both gl_in and gl_out to be gl_PerVertex blocks";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "Init.h"
|
||||
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
||||
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
||||
#include "MG_Impl/GLImpl/Drawing/GL_Drawing.h"
|
||||
#include "MG_Impl/GLImpl/Program/GL_ProgramPipeline.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
|
||||
@@ -243,11 +244,17 @@ TEST_F(ProgramPipelineCompositeTest, AValueIdenticalWriteStillTakesTheSlotForIts
|
||||
DeleteProgramPipelines(1, &pipeline);
|
||||
}
|
||||
|
||||
// glUseProgramStages here accepts a program that was never linked as separable (GL 4.6 core 7.4
|
||||
// says it should not, and MobileGL validates only LINK_STATUS). Such a program has recorded
|
||||
// none of its writes, because nothing ever armed its tracking latch - so the mirror has to fall
|
||||
// back to carrying everything rather than carrying nothing. Mirroring nothing would have been a
|
||||
// fresh regression on a shape that worked before the dirty set existed.
|
||||
// A stage program that recorded NONE of its writes, because nothing ever armed its tracking
|
||||
// latch: the mirror has to fall back to carrying everything rather than carrying nothing.
|
||||
// Mirroring nothing would have been a fresh regression on a shape that worked before the dirty
|
||||
// set existed.
|
||||
//
|
||||
// The shape used to be reachable through glUseProgramStages, which accepted a program that was
|
||||
// never linked as separable. It no longer is: GL 4.6 core 7.4 requires the LATCHED
|
||||
// PROGRAM_SEPARABLE flag and MobileGL now enforces it, and arming that flag is the very thing
|
||||
// that arms the tracking latch - so no program the entry point accepts can be in this state. The
|
||||
// fallback is therefore unreachable from GL and is exercised through the state layer instead,
|
||||
// which is the only way left to keep it covered rather than deleting the coverage with the hole.
|
||||
TEST_F(ProgramPipelineCompositeTest, ANonSeparableStageProgramStillMirrorsItsUniforms) {
|
||||
const char* vsSource = R"(#version 430 core
|
||||
uniform vec4 u_vsOnly;
|
||||
@@ -270,9 +277,19 @@ void main() { gl_Position = u_vsOnly; }
|
||||
GLuint pipeline = 0;
|
||||
GenProgramPipelines(1, &pipeline);
|
||||
BindProgramPipeline(pipeline);
|
||||
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
// The fragment stage goes through the entry point; the vertex one cannot, so it is installed
|
||||
// directly on the pipeline object - the same call glUseProgramStages makes once it is done
|
||||
// validating, minus the validation this shape now fails.
|
||||
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
ASSERT_EQ(GetError(), GL_INVALID_OPERATION)
|
||||
<< "a program not linked as separable is not a legal pipeline stage";
|
||||
{
|
||||
const auto& pipelineObject = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
|
||||
ASSERT_NE(pipelineObject, nullptr);
|
||||
pipelineObject->SetStageProgram(ShaderStage::Vertex, MG_State::pGLContext->GetProgramObject(vs));
|
||||
}
|
||||
|
||||
const float written[4] = {3.0f, 1.0f, 4.0f, 1.0f};
|
||||
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
|
||||
@@ -503,3 +520,205 @@ void main() { o_color = u_shared; }
|
||||
|
||||
UseProgram(0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The vertex stage a pre-rasterization pipeline must have
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// GL 4.6 core 7.4.1: a pipeline whose tessellation-control, tessellation-evaluation or geometry
|
||||
// stage has an executable, but which supplies no executable VERTEX shader, makes every command
|
||||
// that transfers vertices an INVALID_OPERATION. MobileGL checked only "a program is current" and
|
||||
// "it linked", so a geometry+fragment pipeline drew happily and rendered nothing -
|
||||
// KHR-GL4x.geometry_shader.api.fs_gs_draw_call and .pipeline_program_without_active_vs.
|
||||
TEST_F(ProgramPipelineCompositeTest, AGeometryPipelineWithNoVertexStageRefusesToDraw) {
|
||||
const char* kGs = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); }
|
||||
)";
|
||||
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kGs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
||||
|
||||
GLuint pipeline = 0;
|
||||
GenProgramPipelines(1, &pipeline);
|
||||
BindProgramPipeline(pipeline);
|
||||
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
|
||||
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// Not vacuous: the composite has to be a healthy linked program, so that the refusal below
|
||||
// can only be the missing vertex stage and not a link that fell over on its own.
|
||||
const auto composite = DrawProgram();
|
||||
ASSERT_NE(composite, nullptr);
|
||||
ASSERT_TRUE(composite->GetLinkStatus()) << "the composite itself must link for this test to mean anything";
|
||||
ASSERT_TRUE(composite->HasLinkedShaderStage(ShaderStage::Geometry));
|
||||
ASSERT_FALSE(composite->HasLinkedShaderStage(ShaderStage::Vertex));
|
||||
|
||||
DrawArrays(GL_POINTS, 0, 1);
|
||||
EXPECT_EQ(GetError(), GL_INVALID_OPERATION)
|
||||
<< "a geometry stage with no vertex stage must refuse the draw";
|
||||
|
||||
// A dispatch shares the same "is there a program, did it link" helper and legitimately has no
|
||||
// vertex stage; the rule must not have leaked onto it. There is no compute stage here, so the
|
||||
// error is the compute check's own - what matters is that the draw rule did not fire first
|
||||
// with a different meaning.
|
||||
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
|
||||
BindProgramPipeline(0);
|
||||
DeleteProgramPipelines(1, &pipeline);
|
||||
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The composite's transform-feedback capture list.
|
||||
//
|
||||
// Two rules, and getting either wrong turns a working pipeline into one where EVERY draw reports
|
||||
// GL_INVALID_OPERATION: an unresolvable capture name fails the composite's own link, and
|
||||
// ValidateProgramForExecution rejects every draw through a pipeline whose composite did not link -
|
||||
// while glValidateProgramPipeline keeps reporting TRUE.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
const char* kCaptureVs = R"(#version 430 core
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
out float v_captured;
|
||||
out float v_other;
|
||||
void main() { gl_Position = vec4(0.0); v_captured = 1.0; v_other = 2.0; }
|
||||
)";
|
||||
|
||||
// A geometry stage that re-emits nothing the vertex stage named, so a capture list taken from
|
||||
// the VERTEX program cannot resolve against it.
|
||||
const char* kPassthroughGs = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
out float g_only;
|
||||
void main() { gl_Position = vec4(0.0); g_only = 1.0; EmitVertex(); EndPrimitive(); }
|
||||
)";
|
||||
|
||||
Vector<String> CompositeCaptureNames(MG_State::GLState::ProgramObject& composite) {
|
||||
Vector<String> names;
|
||||
for (SizeT i = 0; i < composite.GetTransformFeedbackVaryingCount(); ++i) {
|
||||
if (const auto* varying = composite.GetTransformFeedbackVarying(i)) {
|
||||
names.push_back(varying->name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// glTransformFeedbackVaryings does not take effect until the program's NEXT link (GL 4.6 core
|
||||
// 7.3/11.1.2.1) and deliberately bumps no version, so a request written after the stage program's
|
||||
// last link is invisible to the composite cache's signature - yet the next rebuild would pick it
|
||||
// up. The capture list would then depend on whether some unrelated event happened to invalidate
|
||||
// the cache. Reading the LINKED snapshot removes the whole class, and makes the existing cache key
|
||||
// sufficient: linked state only moves at a link, which is exactly what the key tracks.
|
||||
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureListComesFromTheLinkedSnapshotNotThePendingRequest) {
|
||||
const GLuint vs = CreateProgram();
|
||||
{
|
||||
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(shader, 1, &kCaptureVs, nullptr);
|
||||
CompileShader(shader);
|
||||
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
AttachShader(vs, shader);
|
||||
const char* captured = "v_captured";
|
||||
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
|
||||
LinkProgram(vs);
|
||||
GLint linked = GL_FALSE;
|
||||
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
}
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
||||
|
||||
GLuint pipeline = 0;
|
||||
GenProgramPipelines(1, &pipeline);
|
||||
BindProgramPipeline(pipeline);
|
||||
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
{
|
||||
const auto composite = DrawProgram();
|
||||
ASSERT_NE(composite, nullptr);
|
||||
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}));
|
||||
}
|
||||
|
||||
// A NEW request with no relink. GL says the program still captures v_captured.
|
||||
const char* other = "v_other";
|
||||
TransformFeedbackVaryings(vs, 1, &other, GL_INTERLEAVED_ATTRIBS);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
// Force a composite rebuild through something entirely unrelated to the capture list: a new
|
||||
// fragment stage program moves that slot's lifetime id, so the cache signature changes.
|
||||
const GLuint fs2 = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
||||
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs2);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
{
|
||||
const auto composite = DrawProgram();
|
||||
ASSERT_NE(composite, nullptr);
|
||||
EXPECT_TRUE(composite->GetLinkStatus()) << "the composite must still link";
|
||||
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}))
|
||||
<< "an unlinked request must not reach the composite";
|
||||
}
|
||||
|
||||
// Relinking the stage program IS what makes the new request take effect - and the composite
|
||||
// follows, because the relink moves the link version the cache keys on.
|
||||
LinkProgram(vs);
|
||||
{
|
||||
const auto composite = DrawProgram();
|
||||
ASSERT_NE(composite, nullptr);
|
||||
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_other"}));
|
||||
}
|
||||
|
||||
BindProgramPipeline(0);
|
||||
DeleteProgramPipelines(1, &pipeline);
|
||||
}
|
||||
|
||||
// Transform feedback captures the output of the LAST vertex-processing stage (GL 4.6 core
|
||||
// 11.1.2.1) - the last stage that EXISTS, not the last one that happens to carry a capture list.
|
||||
// Falling through a geometry stage with no request and installing the vertex stage's list instead
|
||||
// made the two halves disagree: this loop picks whose list, the link task resolves those names
|
||||
// against the geometry intermediate. Either it captures where GL says it must not, or the
|
||||
// composite fails to link and every draw through the pipeline reports GL_INVALID_OPERATION.
|
||||
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureStageIsTheLastVertexProcessingStageThatExists) {
|
||||
const GLuint vs = CreateProgram();
|
||||
{
|
||||
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(shader, 1, &kCaptureVs, nullptr);
|
||||
CompileShader(shader);
|
||||
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
AttachShader(vs, shader);
|
||||
const char* captured = "v_captured";
|
||||
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
|
||||
LinkProgram(vs);
|
||||
GLint linked = GL_FALSE;
|
||||
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
}
|
||||
// The geometry program was never given a capture list, and "v_captured" is not one of its
|
||||
// outputs - so a composite seeded from the VERTEX program's list cannot resolve it.
|
||||
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kPassthroughGs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
||||
|
||||
GLuint pipeline = 0;
|
||||
GenProgramPipelines(1, &pipeline);
|
||||
BindProgramPipeline(pipeline);
|
||||
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
|
||||
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
|
||||
const auto composite = DrawProgram();
|
||||
ASSERT_NE(composite, nullptr);
|
||||
EXPECT_TRUE(composite->GetLinkStatus())
|
||||
<< "the geometry stage is the capture stage and has no capture list, so the composite links "
|
||||
"with none - it must not inherit the vertex stage's and fail resolving it";
|
||||
EXPECT_EQ(composite->GetTransformFeedbackVaryingCount(), 0u)
|
||||
<< "the capture stage is the geometry program, which declared nothing to capture";
|
||||
|
||||
BindProgramPipeline(0);
|
||||
DeleteProgramPipelines(1, &pipeline);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -4569,3 +4570,445 @@ subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
<< "an inactive #if arm must not have an unconditional forwarding body appended for it";
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// gl_NumSamples: glslang declares the built-in only when it is NOT targeting SPIR-V, and MobileGL
|
||||
// always targets SPIR-V, so every fragment shader that reads it used to die at compile time with
|
||||
// "'gl_NumSamples' : undeclared identifier". InjectNumSamplesBuiltinShim lowers it onto a reserved
|
||||
// default-block uniform instead; the draw path fills that uniform in.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
Bool HasNumSamplesShim(const String& source) {
|
||||
return source.find("uniform int mg_NumSamples;") != String::npos &&
|
||||
source.find("#define gl_NumSamples mg_NumSamples") != String::npos;
|
||||
}
|
||||
|
||||
void ExpectShaderCompiles(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib attrib{.shaderType = stage, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessFragmentShaderInjectsNumSamplesShim) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// The shape KHR-GL46.sample_variables.mask.* uses: gl_NumSamples as the bound of the loop that
|
||||
// writes gl_SampleMask.
|
||||
String source = R"(#version 460 core
|
||||
layout(location = 0) out highp vec4 o_color;
|
||||
uniform int u_sampleMask;
|
||||
void main() {
|
||||
for (int i = 0; i < (gl_NumSamples + 31) / 32; ++i) {
|
||||
gl_SampleMask[i] = u_sampleMask & gl_SampleMaskIn[i];
|
||||
}
|
||||
o_color = vec4(1, 0, 0, 1);
|
||||
}
|
||||
)";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
|
||||
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, NumSamplesShimIgnoresCommentedAndPartialTokens) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Comment and string text is masked before the token scan, and the scan is whole-identifier:
|
||||
// "gl_NumSamplesFoo" is a different name and must not drag the shim in.
|
||||
String commented = R"(#version 460 core
|
||||
out vec4 fragColor;
|
||||
// gl_NumSamples used to be read here
|
||||
/* gl_NumSamples */
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
String suffixed = R"(#version 460 core
|
||||
out vec4 fragColor;
|
||||
uniform int gl_NumSamplesFoo;
|
||||
void main() { fragColor = vec4(float(gl_NumSamplesFoo)); }
|
||||
)";
|
||||
|
||||
for (String* source : {&commented, &suffixed}) {
|
||||
PreprocessShaderSource(ShaderStage::Fragment, *source);
|
||||
EXPECT_EQ(source->find("mg_NumSamples"), String::npos) << *source;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, NumSamplesShimDoesNotDoubleInject) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Re-running the preprocessor over its own output must be a no-op for this pass; a second
|
||||
// "uniform int mg_NumSamples;" would not compile.
|
||||
String source = R"(#version 460 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gl_NumSamples)); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
ASSERT_TRUE(HasNumSamplesShim(source)) << source;
|
||||
|
||||
const String once = source;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_EQ(source, once) << "the shim re-fired on an already-shimmed source";
|
||||
|
||||
// Same guard for an application that happens to own the name itself.
|
||||
String applicationOwned = R"(#version 460 core
|
||||
uniform int mg_NumSamples;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gl_NumSamples + mg_NumSamples)); }
|
||||
)";
|
||||
const String before = applicationOwned;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, applicationOwned);
|
||||
EXPECT_EQ(applicationOwned, before);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, NumSamplesShimIsFragmentStageOnly) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// gl_NumSamples exists in the fragment stage and nowhere else, so a vertex or geometry source
|
||||
// naming it must be left for glslang to reject rather than quietly legalized.
|
||||
for (const ShaderStage stage : {ShaderStage::Vertex, ShaderStage::Geometry, ShaderStage::Compute}) {
|
||||
String source = R"(#version 460 core
|
||||
out int v;
|
||||
void main() { v = gl_NumSamples; }
|
||||
)";
|
||||
PreprocessShaderSource(stage, source);
|
||||
EXPECT_EQ(source.find("mg_NumSamples"), String::npos) << static_cast<int>(stage) << ":\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, NumSamplesShimHonoursTheVersionAndExtensionGate) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
struct Case {
|
||||
const char* label;
|
||||
const char* versionBlock;
|
||||
Bool expectShim;
|
||||
};
|
||||
// Mirrors glslang's own gate (Initialize.cpp): desktop from 4.00, or from 1.30 with
|
||||
// ARB_sample_shading; ESSL from 3.20, or from 3.10 with OES_sample_variables - which
|
||||
// GL_ANDROID_extension_pack_es31a and `#extension all : warn` also turn on
|
||||
// (TParseVersions::updateExtensionBehavior).
|
||||
const Case cases[] = {
|
||||
{"desktop 460 core", "#version 460 core\n", true},
|
||||
{"desktop 400 core", "#version 400 core\n", true},
|
||||
{"desktop 330 core, no extension", "#version 330 core\n", false},
|
||||
{"desktop 330 core + ARB_sample_shading",
|
||||
"#version 330 core\n#extension GL_ARB_sample_shading : require\n", true},
|
||||
{"desktop 330 core + all : warn",
|
||||
"#version 330 core\n#extension all : warn\n", true},
|
||||
{"desktop 120, no extension", "#version 120\n", false},
|
||||
{"desktop 120 + all : warn (below the 1.30 floor)",
|
||||
"#version 120\n#extension all : warn\n", false},
|
||||
{"ESSL 320", "#version 320 es\n", true},
|
||||
{"ESSL 310, no extension", "#version 310 es\n", false},
|
||||
{"ESSL 310 + OES_sample_variables",
|
||||
"#version 310 es\n#extension GL_OES_sample_variables : require\n", true},
|
||||
// The AEP spellings. glslang applies the directive's behavior to all twelve AEP members,
|
||||
// GL_OES_sample_variables among them, so these are legal ES 3.1 shaders.
|
||||
{"ESSL 310 + AEP : require",
|
||||
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : require\n", true},
|
||||
{"ESSL 310 + AEP : enable",
|
||||
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : enable\n", true},
|
||||
{"ESSL 310 + AEP : warn",
|
||||
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : warn\n", true},
|
||||
// ...but `disable` is not an opt-in, and the implication carries the behavior with it.
|
||||
{"ESSL 310 + AEP : disable",
|
||||
"#version 310 es\n#extension GL_ANDROID_extension_pack_es31a : disable\n", false},
|
||||
{"ESSL 310 + all : warn",
|
||||
"#version 310 es\n#extension all : warn\n", true},
|
||||
// An AEP member that does NOT imply sample variables must not open the gate.
|
||||
{"ESSL 310 + EXT_geometry_shader only",
|
||||
"#version 310 es\n#extension GL_EXT_geometry_shader : require\n", false},
|
||||
{"ESSL 300", "#version 300 es\n", false},
|
||||
{"ESSL 300 + AEP (below the 3.10 floor)",
|
||||
"#version 300 es\n#extension GL_ANDROID_extension_pack_es31a : require\n", false},
|
||||
};
|
||||
|
||||
for (const Case& testCase : cases) {
|
||||
SCOPED_TRACE(testCase.label);
|
||||
String source = String(testCase.versionBlock) + R"(out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gl_NumSamples)); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_EQ(HasNumSamplesShim(source), testCase.expectShim) << source;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// ES preamble extension macros. Rewriting "#version 310 es" to "#version 460 core" makes glslang
|
||||
// emit its DESKTOP preamble, which defines none of the OES/AEP extension macros - so a shader's
|
||||
// own "#if !GL_OES_sample_variables" guard takes the branch it was written to avoid. The macros
|
||||
// travel through glslang's CUSTOM PREAMBLE rather than the shader text, because "#define GL_..."
|
||||
// in an application-supplied string is a hard error (reservedPpErrorCheck).
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProgramUtilTest, EsSourceRegainsThePreambleMacrosForTheExtensionsItNames) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// KHR-GL46.es_31_compatibility.sample_variables.verification.extension in miniature: the
|
||||
// deliberately-broken arm must stay unreached.
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_OES_sample_variables : enable
|
||||
precision highp float;
|
||||
out vec4 fragColor;
|
||||
#if !GL_OES_sample_variables
|
||||
this is broken
|
||||
#endif
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_EQ(CollectEsPreambleMacroDefines(source), String("#define GL_OES_sample_variables 1\n")) << source;
|
||||
// And the compiler really does feed it to glslang: without the preamble this source takes the
|
||||
// "this is broken" arm and dies on a reserved word.
|
||||
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, EsPreambleMacroInjectionStaysNarrow) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
{
|
||||
SCOPED_TRACE("only the extensions the source names, and never GL_ES");
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_OES_sample_variables : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
const String defines = CollectEsPreambleMacroDefines(source);
|
||||
EXPECT_EQ(defines.find("GL_OES_shader_image_atomic"), String::npos) << defines;
|
||||
// GL_ES stays undefined on purpose: the shader really is compiled as desktop now, and
|
||||
// flipping "#ifdef GL_ES" branches would break far more than it fixes.
|
||||
EXPECT_EQ(defines.find("#define GL_ES "), String::npos) << defines;
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("an extension glslang's DESKTOP preamble already defines is not re-defined");
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_EXT_shader_non_constant_global_initializers : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
// Nothing to restore, so the source is not even marked.
|
||||
EXPECT_EQ(source.find("mobilegl-es-preamble"), String::npos) << source;
|
||||
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a desktop source is untouched - it keeps the preamble it is entitled to");
|
||||
String source = R"(#version 460 core
|
||||
#extension GL_OES_sample_variables : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
const String before = source;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_EQ(source, before);
|
||||
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a shader that merely contains the marker text cannot steer the preamble");
|
||||
String source = R"(#version 460 core
|
||||
/*mobilegl-es-preamble:310*/
|
||||
#extension GL_OES_sample_variables : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
// The extractor is honest about what it finds - a source carrying a well-formed marker is
|
||||
// indistinguishable from one this pipeline wrote, which is exactly why the payload is
|
||||
// re-derived from the whitelist here rather than read out of the marker.
|
||||
EXPECT_EQ(CollectEsPreambleMacroDefines(source), String("#define GL_OES_sample_variables 1\n"));
|
||||
|
||||
String malformed = R"(#version 460 core
|
||||
/*mobilegl-es-preamble:not-a-version*/
|
||||
#extension GL_OES_sample_variables : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
EXPECT_TRUE(CollectEsPreambleMacroDefines(malformed).empty());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// A repeated #version directive. glShaderSource concatenates its strings with nothing added
|
||||
// between them (GL 4.6 core 7.1), so a caller that heads BOTH strings with a #version splices the
|
||||
// second into the tail of the first - which is what VK-GL-CTS's ShaderImageLoadStoreBase::
|
||||
// BuildProgram does.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(ProgramUtilTest, RepeatedIdenticalVersionDirectiveIsElided) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// Byte-for-byte the concatenation the CTS produces: kGLSLPrec ends without a newline, so the
|
||||
// subcase's own "#version 310 es" lands mid-line.
|
||||
String source =
|
||||
"#version 310 es\n\nprecision highp float;\nprecision highp uimage2DArray;#version 310 es\n"
|
||||
"layout(location = 0) in vec4 i_position;\n"
|
||||
"void main() { gl_Position = i_position; }\n";
|
||||
|
||||
const SizeT lineCountBefore = static_cast<SizeT>(std::count(source.begin(), source.end(), '\n'));
|
||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||
|
||||
// Exactly one #version survives, and the line count is untouched so __LINE__ and every
|
||||
// glslang diagnostic still point where the application wrote them.
|
||||
EXPECT_EQ(source.find("#version", source.find("#version") + 1), String::npos) << source;
|
||||
EXPECT_EQ(static_cast<SizeT>(std::count(source.begin(), source.end(), '\n')), lineCountBefore) << source;
|
||||
ExpectShaderCompiles(GL_VERTEX_SHADER, source);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, OnlyAnExactVersionRepeatIsElided) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a DIFFERENT second version is left for glslang to reject");
|
||||
String source =
|
||||
"#version 310 es\nprecision highp float;\n#version 320 es\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_NE(source.find("#version 320 es"), String::npos) << source;
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a lone non-first #version is still a lone non-first #version");
|
||||
// KHR-GL33.shaders.preprocessor.directive.version_not_first_statement_1 requires this to
|
||||
// fail to compile, and it only does so because the directive is left where it was.
|
||||
String source =
|
||||
"precision mediump float;\n#version 330\nout vec4 c;\nvoid main() { c = vec4(1.0); }\n";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
const SizeT versionPos = source.find("#version");
|
||||
ASSERT_NE(versionPos, String::npos) << source;
|
||||
EXPECT_NE(versionPos, SizeT{0}) << "the directive must not have been moved to the front:\n" << source;
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
EXPECT_FALSE(res.has_value()) << "a #version preceded by real tokens must still be rejected:\n" << source;
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a MALFORMED repeat is left alone");
|
||||
String source =
|
||||
"#version 330 core\nout vec4 c;\n#version 330 foobar\nvoid main() { c = vec4(1.0); }\n";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_NE(source.find("#version 330 foobar"), String::npos) << source;
|
||||
}
|
||||
}
|
||||
|
||||
// glslang applies an #extension directive's behavior to every extension the named one IMPLIES
|
||||
// (TParseVersions::updateExtensionBehavior, Versions.cpp:1039-1064). Both consumers of the
|
||||
// extension sets have to see that expansion or the AEP spelling of a shader behaves differently
|
||||
// from the byte-equivalent one that names its members directly.
|
||||
TEST_F(ProgramUtilTest, AepFansOutToItsMemberExtensionMacros) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
// The CTS-shaped guard, opted in the AEP way. Before the fan-out this took the broken arm.
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_ANDROID_extension_pack_es31a : require
|
||||
precision highp float;
|
||||
out vec4 fragColor;
|
||||
#if !GL_OES_sample_variables
|
||||
this is broken
|
||||
#endif
|
||||
#if !GL_OES_shader_multisample_interpolation
|
||||
this is also broken
|
||||
#endif
|
||||
void main() { fragColor = vec4(float(gl_NumSamples)); }
|
||||
)";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
// Both halves of the AEP path: the built-in shim AND the restored member macros.
|
||||
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
|
||||
const String defines = CollectEsPreambleMacroDefines(source);
|
||||
for (const char* member : {"GL_ANDROID_extension_pack_es31a", "GL_OES_sample_variables",
|
||||
"GL_OES_shader_image_atomic", "GL_OES_shader_multisample_interpolation",
|
||||
"GL_OES_texture_storage_multisample_2d_array", "GL_EXT_geometry_shader",
|
||||
"GL_EXT_gpu_shader5", "GL_EXT_primitive_bounding_box",
|
||||
"GL_EXT_shader_io_blocks", "GL_EXT_tessellation_shader",
|
||||
"GL_EXT_texture_buffer", "GL_EXT_texture_cube_map_array"}) {
|
||||
EXPECT_NE(defines.find(String("#define ") + member + " 1\n"), String::npos)
|
||||
<< member << " missing from:\n" << defines;
|
||||
}
|
||||
// GL_KHR_blend_equation_advanced is an AEP member glslang propagates to, but its macro is in
|
||||
// the DESKTOP preamble too - so the rewrite never took it away and it must not be restored.
|
||||
EXPECT_EQ(defines.find("GL_KHR_blend_equation_advanced"), String::npos) << defines;
|
||||
|
||||
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, ExtensionImplicationIsTransitiveAndStaysNamed) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
{
|
||||
SCOPED_TRACE("geometry/tessellation imply the matching io_blocks");
|
||||
// glslang re-enters updateExtensionBehavior for each implication, so the graph is walked
|
||||
// to a fixed point rather than one level deep.
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_OES_geometry_shader : require
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
const String defines = CollectEsPreambleMacroDefines(source);
|
||||
EXPECT_NE(defines.find("#define GL_OES_geometry_shader 1\n"), String::npos) << defines;
|
||||
EXPECT_NE(defines.find("#define GL_OES_shader_io_blocks 1\n"), String::npos) << defines;
|
||||
// The EXT spelling is a different extension and must not come along.
|
||||
EXPECT_EQ(defines.find("GL_EXT_shader_io_blocks"), String::npos) << defines;
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("a source that names nothing implied still gets nothing");
|
||||
String source = R"(#version 310 es
|
||||
#extension GL_OES_sample_variables : enable
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
const String defines = CollectEsPreambleMacroDefines(source);
|
||||
EXPECT_EQ(defines, String("#define GL_OES_sample_variables 1\n")) << defines;
|
||||
}
|
||||
|
||||
{
|
||||
SCOPED_TRACE("`all` opens the built-in gate but does not define every ES macro");
|
||||
// The two questions differ: `all : warn` really does turn every extension on in glslang,
|
||||
// but the preamble macros are defined before any #extension line runs, so `all` says
|
||||
// nothing about which ones the ES -> desktop rewrite took away.
|
||||
String source = R"(#version 310 es
|
||||
#extension all : warn
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gl_NumSamples)); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_TRUE(HasNumSamplesShim(source)) << source;
|
||||
EXPECT_EQ(source.find("mobilegl-es-preamble"), String::npos) << source;
|
||||
EXPECT_TRUE(CollectEsPreambleMacroDefines(source).empty());
|
||||
}
|
||||
}
|
||||
|
||||
// The mid-line #version probe must search THE LINE, not the rest of the file: an unbounded
|
||||
// std::string::find makes InspectShaderLanguage quadratic on the ordinary resolved-shader-pack
|
||||
// shape (one leading #version, no further '#' anywhere). This pins both halves - the detection
|
||||
// still fires, and it fires on a source whose only other content is a long directive-free body.
|
||||
TEST_F(ProgramUtilTest, MidLineVersionDetectionSurvivesALongDirectiveFreeBody) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String body;
|
||||
body.reserve(64 * 1024);
|
||||
for (int line = 0; line < 2000; ++line) {
|
||||
body += " float v" + std::to_string(line) + " = 0.0;\n";
|
||||
}
|
||||
|
||||
// The CTS concatenation shape, followed by a body with no '#' in it at all.
|
||||
String source = "#version 310 es\nprecision highp float;#version 310 es\nout vec4 fragColor;\nvoid main() {\n" +
|
||||
body + " fragColor = vec4(1.0);\n}\n";
|
||||
const SizeT lineCountBefore = static_cast<SizeT>(std::count(source.begin(), source.end(), '\n'));
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
EXPECT_EQ(source.find("#version", source.find("#version") + 1), String::npos) << source.substr(0, 200);
|
||||
EXPECT_EQ(static_cast<SizeT>(std::count(source.begin(), source.end(), '\n')), lineCountBefore);
|
||||
ExpectShaderCompiles(GL_FRAGMENT_SHADER, source);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <ios>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
@@ -814,3 +815,113 @@ TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
||||
}
|
||||
EXPECT_EQ(MG_Config::Features.DisableTimerQuery, expected);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// GL_ARB_pipeline_statistics_query, core since 4.6. The eleven counter targets had no arm in
|
||||
// glBeginQuery's accepted-target list, so the very first glBeginQuery(GL_VERTICES_SUBMITTED)
|
||||
// raised GL_INVALID_ENUM and killed
|
||||
// pipeline_statistics_query_tests_ARB.api_coverage_invalid_glbeginquery_calls before it could
|
||||
// check anything. MobileGL instruments none of the counters and says so through the mechanism
|
||||
// GL 4.6 core 4.2.1 provides for exactly this: GL_QUERY_COUNTER_BITS = 0, which the conformance
|
||||
// suite reads and treats as "skip the functional half of this target".
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
TEST_F(QueryTest, PipelineStatisticsTargetsAreAcceptedAndReportZeroCounterBits) {
|
||||
static constexpr GLenum kTargets[] = {
|
||||
GL_VERTICES_SUBMITTED, GL_PRIMITIVES_SUBMITTED,
|
||||
GL_VERTEX_SHADER_INVOCATIONS, GL_TESS_CONTROL_SHADER_PATCHES,
|
||||
GL_TESS_EVALUATION_SHADER_INVOCATIONS, GL_GEOMETRY_SHADER_INVOCATIONS,
|
||||
GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED, GL_FRAGMENT_SHADER_INVOCATIONS,
|
||||
GL_COMPUTE_SHADER_INVOCATIONS, GL_CLIPPING_INPUT_PRIMITIVES,
|
||||
GL_CLIPPING_OUTPUT_PRIMITIVES,
|
||||
};
|
||||
|
||||
for (const GLenum target: kTargets) {
|
||||
GLuint id = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||
ASSERT_NE(id, 0u);
|
||||
|
||||
MG_Impl::GLImpl::BeginQuery(target, id);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< "glBeginQuery must accept pipeline-statistics target 0x" << std::hex << target;
|
||||
|
||||
GLint current = 0;
|
||||
MG_Impl::GLImpl::GetQueryiv(target, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(static_cast<GLuint>(current), id) << "GL_CURRENT_QUERY has to track this target too";
|
||||
|
||||
MG_Impl::GLImpl::EndQuery(target);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsQuery(id), GL_TRUE);
|
||||
|
||||
GLint counterBits = -1;
|
||||
MG_Impl::GLImpl::GetQueryiv(target, GL_QUERY_COUNTER_BITS, &counterBits);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
EXPECT_EQ(counterBits, 0) << "an uninstrumented counter reports zero bits, per GL 4.6 core 4.2.1";
|
||||
|
||||
// The result is immediately available (nothing was ever submitted to wait on) and reads
|
||||
// as the zero the zero counter-bit answer marks indeterminate.
|
||||
GLuint available = 0;
|
||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
||||
EXPECT_EQ(available, static_cast<GLuint>(GL_TRUE));
|
||||
GLuint result = 0xDEADBEEFu;
|
||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
||||
EXPECT_EQ(result, 0u);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||
}
|
||||
}
|
||||
|
||||
// The negative half the conformance case actually asserts: an object already latched onto one
|
||||
// pipeline-statistics target must refuse a different one with GL_INVALID_OPERATION. This is what
|
||||
// per-target active slots buy - a single shared slot would have reported "a query is already
|
||||
// active on this target" for an unrelated target instead.
|
||||
TEST_F(QueryTest, PipelineStatisticsQueryObjectRefusesASecondTargetAndTargetsAreIndependent) {
|
||||
GLuint id = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
||||
ASSERT_NE(id, 0u);
|
||||
|
||||
MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, id);
|
||||
MG_Impl::GLImpl::EndQuery(GL_VERTICES_SUBMITTED);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, id);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
|
||||
// Two different objects on two different targets are simultaneously active, because each
|
||||
// target owns its own slot.
|
||||
GLuint first = 0;
|
||||
GLuint second = 0;
|
||||
MG_Impl::GLImpl::GenQueries(1, &first);
|
||||
MG_Impl::GLImpl::GenQueries(1, &second);
|
||||
MG_Impl::GLImpl::BeginQuery(GL_VERTICES_SUBMITTED, first);
|
||||
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_SUBMITTED, second);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint current = 0;
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(static_cast<GLuint>(current), first);
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(static_cast<GLuint>(current), second);
|
||||
|
||||
// Deleting an ACTIVE query implicitly ends it and releases its slot; the sibling target is
|
||||
// untouched.
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &first);
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_VERTICES_SUBMITTED, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(current, 0);
|
||||
MG_Impl::GLImpl::GetQueryiv(GL_PRIMITIVES_SUBMITTED, GL_CURRENT_QUERY, ¤t);
|
||||
EXPECT_EQ(static_cast<GLuint>(current), second);
|
||||
|
||||
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_SUBMITTED);
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &second);
|
||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
||||
while (MG_Impl::GLImpl::GetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// glCreateQueries keeps its own, shorter accepted-target list on purpose: it is unchanged here,
|
||||
// and this pins that the pipeline-statistics addition did not leak into it.
|
||||
TEST_F(QueryTest, EndQueryOnAPipelineStatisticsTargetWithNoActiveQueryIsInvalidOperation) {
|
||||
MG_Impl::GLImpl::EndQuery(GL_FRAGMENT_SHADER_INVOCATIONS);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
@@ -36,13 +36,21 @@
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Types.h>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
|
||||
namespace {
|
||||
class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject {
|
||||
public:
|
||||
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params):
|
||||
m_params(params) {}
|
||||
// `type` defaults to Unknown, which is what every existing case wanted: a limits-only
|
||||
// double with no backend identity. A case that captures a CompileEnv from it and then
|
||||
// compares the result against glGetIntegerv has to pass a REAL type, because
|
||||
// CompileEnv::HasBackend() is what BuildTBuiltInResource bounds gl_MaxVertexAttribs by
|
||||
// while the getter bounds it by "a backend object exists" - two spellings of the same
|
||||
// thing in production, and only in production.
|
||||
explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params,
|
||||
MobileGL::BackendType type = MobileGL::BackendType::Unknown):
|
||||
m_params(params), m_type(type) {}
|
||||
|
||||
void Initialize() override {}
|
||||
MobileGL::Bool InitCapabilities() override { return true; }
|
||||
@@ -55,10 +63,11 @@ namespace {
|
||||
const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override {
|
||||
return m_params;
|
||||
}
|
||||
MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; }
|
||||
MobileGL::BackendType GetBackendType() const override { return m_type; }
|
||||
|
||||
private:
|
||||
MobileGL::MG_Backend::DynamicBackendParameters m_params;
|
||||
MobileGL::BackendType m_type = MobileGL::BackendType::Unknown;
|
||||
MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{};
|
||||
MobileGL::RendererInfo m_info{
|
||||
.RendererName = "Test",
|
||||
@@ -722,6 +731,62 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||
}
|
||||
|
||||
// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from
|
||||
// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and
|
||||
// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at
|
||||
// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource
|
||||
// with no device consulted at all, which let glslang accept a gl_CullDistance write that then
|
||||
// discarded every primitive it touched.
|
||||
TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||
MG_External::VulkanCapabilities caps;
|
||||
caps.MaxClipDistances = 8;
|
||||
caps.MaxCullDistances = 8;
|
||||
caps.MaxCombinedClipAndCullDistances = 8;
|
||||
|
||||
caps.SupportsShaderClipDistance = false;
|
||||
caps.SupportsShaderCullDistance = false;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
|
||||
|
||||
// Clip only: cull stays zero, and the combined limit still describes the clip capacity.
|
||||
caps.SupportsShaderClipDistance = true;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
|
||||
|
||||
caps.SupportsShaderCullDistance = true;
|
||||
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8);
|
||||
}
|
||||
|
||||
// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the
|
||||
// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the
|
||||
// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it.
|
||||
TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
|
||||
MG_External::GLESCapabilities caps;
|
||||
backend.ApplyGLESCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0);
|
||||
|
||||
caps.SupportsClipDistance = true;
|
||||
caps.MaxClipDistances = 8;
|
||||
caps.MaxCullDistances = 8;
|
||||
caps.MaxCombinedClipAndCullDistances = 8;
|
||||
backend.ApplyGLESCapabilitiesForTesting(caps);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8);
|
||||
EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 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
|
||||
@@ -1234,6 +1299,256 @@ void main() {
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// THE invariant every KHR-GL45.limits.* case checks, in one place. When the conformance table
|
||||
// gives a limit both a glGetIntegerv pname and a GLSL built-in constant, it reads the query and
|
||||
// then compiles a shader that writes the built-in into an SSBO and demands EXACT equality - so a
|
||||
// limit answered from two unreconciled tables fails the SECOND half of the case, with a message
|
||||
// about a number rather than about the two tables. Seven of them did: gl_MaxVertexAttribs said 64
|
||||
// against a query of 32, gl_MaxDrawBuffers 32 against 8, gl_MaxCombinedTextureImageUnits 80
|
||||
// against 96, gl_MaxVaryingComponents 60 against 64, gl_MaxCombinedShaderOutputResources 8
|
||||
// against 29.
|
||||
//
|
||||
// KEEP THIS TABLE GROWING. Every pname added to GL_Getter that also has a gl_Max* built-in
|
||||
// belongs here; that is what stops the next one from drifting.
|
||||
TEST(GetterSanity, EveryLimitWithABuiltinAgreesWithItsQuery) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::pActiveBackendObject =
|
||||
MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{}, BackendType::DirectGLES);
|
||||
|
||||
struct LimitPair {
|
||||
GLenum pname;
|
||||
const char* builtin;
|
||||
};
|
||||
const LimitPair pairs[] = {
|
||||
{GL_MAX_VERTEX_ATTRIBS, "gl_MaxVertexAttribs"},
|
||||
{GL_MAX_VERTEX_UNIFORM_COMPONENTS, "gl_MaxVertexUniformComponents"},
|
||||
{GL_MAX_VERTEX_UNIFORM_VECTORS, "gl_MaxVertexUniformVectors"},
|
||||
{GL_MAX_VERTEX_OUTPUT_COMPONENTS, "gl_MaxVertexOutputComponents"},
|
||||
{GL_MAX_VARYING_COMPONENTS, "gl_MaxVaryingComponents"},
|
||||
{GL_MAX_VARYING_VECTORS, "gl_MaxVaryingVectors"},
|
||||
{GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, "gl_MaxVertexTextureImageUnits"},
|
||||
{GL_MAX_TEXTURE_IMAGE_UNITS, "gl_MaxTextureImageUnits"},
|
||||
{GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "gl_MaxCombinedTextureImageUnits"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, "gl_MaxFragmentUniformComponents"},
|
||||
{GL_MAX_FRAGMENT_UNIFORM_VECTORS, "gl_MaxFragmentUniformVectors"},
|
||||
{GL_MAX_FRAGMENT_INPUT_COMPONENTS, "gl_MaxFragmentInputComponents"},
|
||||
{GL_MAX_DRAW_BUFFERS, "gl_MaxDrawBuffers"},
|
||||
{GL_MAX_IMAGE_UNITS, "gl_MaxImageUnits"},
|
||||
// The SAME token (0x8F39) under two spellings, and the two glslang fields behind them
|
||||
// must therefore carry the same value.
|
||||
{GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS, "gl_MaxCombinedImageUnitsAndFragmentOutputs"},
|
||||
{GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES, "gl_MaxCombinedShaderOutputResources"},
|
||||
{GL_MAX_CLIP_DISTANCES, "gl_MaxClipDistances"},
|
||||
{GL_MAX_CULL_DISTANCES, "gl_MaxCullDistances"},
|
||||
{GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, "gl_MaxCombinedClipAndCullDistances"},
|
||||
{GL_MAX_SAMPLES, "gl_MaxSamples"},
|
||||
{GL_MIN_PROGRAM_TEXEL_OFFSET, "gl_MinProgramTexelOffset"},
|
||||
{GL_MAX_PROGRAM_TEXEL_OFFSET, "gl_MaxProgramTexelOffset"},
|
||||
{GL_MAX_GEOMETRY_INPUT_COMPONENTS, "gl_MaxGeometryInputComponents"},
|
||||
{GL_MAX_GEOMETRY_OUTPUT_COMPONENTS, "gl_MaxGeometryOutputComponents"},
|
||||
{GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS, "gl_MaxGeometryTextureImageUnits"},
|
||||
{GL_MAX_GEOMETRY_OUTPUT_VERTICES, "gl_MaxGeometryOutputVertices"},
|
||||
{GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS, "gl_MaxGeometryTotalOutputComponents"},
|
||||
{GL_MAX_GEOMETRY_UNIFORM_COMPONENTS, "gl_MaxGeometryUniformComponents"},
|
||||
{GL_MAX_PATCH_VERTICES, "gl_MaxPatchVertices"},
|
||||
{GL_MAX_TESS_GEN_LEVEL, "gl_MaxTessGenLevel"},
|
||||
{GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "gl_MaxTessControlInputComponents"},
|
||||
{GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "gl_MaxTessControlOutputComponents"},
|
||||
{GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "gl_MaxTessControlTextureImageUnits"},
|
||||
{GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "gl_MaxTessControlUniformComponents"},
|
||||
{GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "gl_MaxTessControlTotalOutputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "gl_MaxTessEvaluationInputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "gl_MaxTessEvaluationOutputComponents"},
|
||||
{GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "gl_MaxTessEvaluationTextureImageUnits"},
|
||||
{GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "gl_MaxTessEvaluationUniformComponents"},
|
||||
{GL_MAX_TESS_PATCH_COMPONENTS, "gl_MaxTessPatchComponents"},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "gl_MaxTransformFeedbackBuffers"},
|
||||
{GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "gl_MaxTransformFeedbackInterleavedComponents"},
|
||||
// gl_MaxAtomicCounterBindings is glslang's name for the binding count; the GL spelling is
|
||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS.
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "gl_MaxAtomicCounterBindings"},
|
||||
{GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "gl_MaxAtomicCounterBufferSize"},
|
||||
};
|
||||
|
||||
// The compile runs against a captured env, exactly as the pipeline's does - that is what
|
||||
// makes "the resource table" mean the same thing here as it does in production.
|
||||
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||
for (const LimitPair& pair : pairs) {
|
||||
GLint reported = -424242;
|
||||
MG_Impl::GLImpl::GetIntegerv(pair.pname, &reported);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
|
||||
<< pair.builtin << "'s pname is not answerable at all";
|
||||
|
||||
// A negative array size is a compile error, so the stage only compiles when the built-in
|
||||
// equals what the query just reported. Two-sided by construction: a resource table that
|
||||
// is too permissive fails it exactly like one that is too tight. One shader per pair, so
|
||||
// a failure names the limit instead of reporting "something disagreed".
|
||||
const String source = String("#version 460 core\nout vec4 mgColor;\nconst int mgAgree = (") +
|
||||
pair.builtin + " == " + std::to_string(reported) +
|
||||
") ? 1 : -1;\nint mgProbe[mgAgree];\nvoid main() { mgProbe[0] = 0; mgColor = "
|
||||
"vec4(float(mgProbe[0])); }\n";
|
||||
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_FRAGMENT_SHADER,
|
||||
.sourceStr = source,
|
||||
.env = env.get(),
|
||||
});
|
||||
EXPECT_TRUE(compiled) << pair.builtin << " does not equal glGetIntegerv's " << reported << ":\n"
|
||||
<< (compiled ? String() : compiled.error().log);
|
||||
}
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL_MAX_ELEMENT_INDEX is 64-bit state whose required value (2^32-1) does not fit a GLint, so it
|
||||
// needs its own case in BOTH widths: the 64-bit query has to answer 4294967295 and the 32-bit one
|
||||
// has to saturate, per the GL state-query conversion rules. It used to be a single `1024 * 1024;
|
||||
// // TODO` in the 32-bit table, and glGetInteger64v - which is how the conformance suite reads it
|
||||
// - widened that.
|
||||
TEST(GetterSanity, MaxElementIndexIsTheFull32BitIndexCeiling) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
|
||||
|
||||
GLint64 wide = -1;
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_MAX_ELEMENT_INDEX, &wide);
|
||||
EXPECT_EQ(wide, static_cast<GLint64>(0xFFFFFFFFLL));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
GLint narrow = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ELEMENT_INDEX, &narrow);
|
||||
EXPECT_EQ(narrow, INT32_MAX) << "the 32-bit query must saturate, not truncate or wrap";
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 gives GL_MAX_SAMPLES a minimum of four and the three per-category
|
||||
// ceilings a minimum of ONE. Flooring the latter at four is the advertised-caps lie that made
|
||||
// KHR-GL46.sample_variables.mask.rgba8i run at all: the frontend promised four integer samples,
|
||||
// the backend clamped the realised allocation to the one the driver can back, and the application
|
||||
// wrote per-sample data it could never read.
|
||||
TEST(GetterSanity, PerCategoryMultisampleCeilingsAreProbedRatherThanFlooredAtFour) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MaxSamples = 4;
|
||||
params.MaxColorTextureSamples = 4;
|
||||
params.MaxDepthTextureSamples = 2;
|
||||
params.MaxIntegerSamples = 1;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLint reported = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_INTEGER_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 1) << "an integer multisample texture is backed by one sample here, and "
|
||||
"saying otherwise is what the application allocates against";
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 2);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 4);
|
||||
// ...while GL_MAX_SAMPLES keeps its floor of four, which is the one the spec really requires.
|
||||
params.MaxSamples = 1;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &reported);
|
||||
EXPECT_EQ(reported, 4);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
// GL_ARB_cull_distance below #version 450, which is the band the conformance suite actually
|
||||
// compiles in: cull_distance.coverage emits its compute shader at "#version 420 core" with
|
||||
// `#extension GL_ARB_cull_distance : require` and reads gl_MaxCullDistances. Registering the
|
||||
// extension name alone was not enough - `require` started succeeding while the constants stayed
|
||||
// gated on 450, so the shader traded one error for another.
|
||||
//
|
||||
// The three cases below are the whole contract: the macro must be true exactly where the feature
|
||||
// is, the constants must exist under the extension, and using the feature WITHOUT the extension
|
||||
// must still fail (otherwise the gate is decorative).
|
||||
TEST(ShaderCompilerSanity, ArbCullDistanceIsUsableBelow450) {
|
||||
using namespace MobileGL;
|
||||
|
||||
auto previousContext = Move(MG_State::pGLContext);
|
||||
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(MG_Backend::DynamicBackendParameters{});
|
||||
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||
|
||||
const auto compileFragment = [&env](const String& source) {
|
||||
return MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||
.shaderType = GL_FRAGMENT_SHADER,
|
||||
.sourceStr = source,
|
||||
.env = env.get(),
|
||||
});
|
||||
};
|
||||
|
||||
// The coverage shader's shape, reduced to a fragment stage: require the extension, then read
|
||||
// the constant it brings.
|
||||
const String withExtension = R"(#version 420 core
|
||||
#extension GL_ARB_cull_distance : require
|
||||
out vec4 mgColor;
|
||||
void main() { mgColor = vec4(float(gl_MaxCullDistances + gl_MaxCombinedClipAndCullDistances)); }
|
||||
)";
|
||||
auto extensionCompiled = compileFragment(withExtension);
|
||||
EXPECT_TRUE(extensionCompiled) << (extensionCompiled ? String() : extensionCompiled.error().log);
|
||||
|
||||
// The macro has to agree with that, or the standard `#ifdef` probe lies in one direction or
|
||||
// the other. It is defined from 400 up, where the built-ins exist...
|
||||
const String macroProbe420 = R"(#version 420 core
|
||||
out vec4 mgColor;
|
||||
#ifndef GL_ARB_cull_distance
|
||||
#error GL_ARB_cull_distance should be defined at 420
|
||||
#endif
|
||||
void main() { mgColor = vec4(0.0); }
|
||||
)";
|
||||
auto macro420 = compileFragment(macroProbe420);
|
||||
EXPECT_TRUE(macro420) << (macro420 ? String() : macro420.error().log);
|
||||
|
||||
// ...and NOT below it, where they do not. A shader whose `#ifdef GL_ARB_cull_distance` branch
|
||||
// reads gl_MaxCullDistances used to take that branch at 330 and fail to compile.
|
||||
const String macroProbe330 = R"(#version 330 core
|
||||
out vec4 mgColor;
|
||||
#ifdef GL_ARB_cull_distance
|
||||
#error GL_ARB_cull_distance must not be advertised where the built-ins do not exist
|
||||
#endif
|
||||
void main() { mgColor = vec4(0.0); }
|
||||
)";
|
||||
auto macro330 = compileFragment(macroProbe330);
|
||||
EXPECT_TRUE(macro330) << (macro330 ? String() : macro330.error().log);
|
||||
|
||||
// The gate is real: below 450 the constants are reachable ONLY through the extension.
|
||||
const String withoutExtension = R"(#version 420 core
|
||||
out vec4 mgColor;
|
||||
void main() { mgColor = vec4(float(gl_MaxCullDistances)); }
|
||||
)";
|
||||
EXPECT_FALSE(compileFragment(withoutExtension))
|
||||
<< "gl_MaxCullDistances must require GL_ARB_cull_distance below #version 450";
|
||||
|
||||
// ...and at 450 it is core, so no directive is needed.
|
||||
const String core450 = R"(#version 450 core
|
||||
out vec4 mgColor;
|
||||
void main() { mgColor = vec4(float(gl_MaxCullDistances)); }
|
||||
)";
|
||||
auto coreCompiled = compileFragment(core450);
|
||||
EXPECT_TRUE(coreCompiled) << (coreCompiled ? String() : coreCompiled.error().log);
|
||||
|
||||
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||
MG_State::pGLContext = Move(previousContext);
|
||||
}
|
||||
|
||||
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -2745,3 +3060,61 @@ TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMi
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY));
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
|
||||
}
|
||||
|
||||
// GL_MAX_COMBINED_*_UNIFORM_COMPONENTS is components + blocks * (blockSize / 4). The product was
|
||||
// formed in signed 32-bit, and a Vulkan host that reports a large VkPhysicalDeviceLimits::
|
||||
// maxUniformBufferRange (a Mali driver answers 0xFFFFFFFF, which the loader saturates to
|
||||
// INT32_MAX) made 14 * (2147483647 / 4) + 4096 wrap to -1073737742 - which is byte for byte what
|
||||
// the conformance suite read back as "Limit value is: -1073737742 when it should not be smaller
|
||||
// than 58368". GLES escaped it only because the ES driver answers 65536 for the block size.
|
||||
TEST(GetterSanity, CombinedUniformComponentsSaturateInsteadOfOverflowing) {
|
||||
using namespace MobileGL;
|
||||
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
|
||||
// GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS (0x8266), NOT the per-stage
|
||||
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS (0x8263) this list used to name. The per-stage token is
|
||||
// answered by a frontend constant and never reaches GetMaxCombinedUniformComponents at all, so
|
||||
// both assertions on it were vacuous - and it displaced the ONE reader whose block count comes
|
||||
// from the backend (ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks)) rather
|
||||
// than from a frontend constant, i.e. the only call site where the saturation actually depends
|
||||
// on data a driver supplies.
|
||||
static constexpr GLenum kCombinedPnames[] = {
|
||||
GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS,
|
||||
GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS, GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS,
|
||||
GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS, GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS,
|
||||
};
|
||||
// The GL 4.6 core table 23.64 floor, which all six combined pnames carry.
|
||||
static constexpr GLint kCombinedFloor = 58368;
|
||||
|
||||
{
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MaxUniformBlockSize = std::numeric_limits<GLint>::max();
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
for (const GLenum pname: kCombinedPnames) {
|
||||
GLint reported = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
|
||||
EXPECT_GT(reported, 0) << "pname 0x" << pname << " wrapped to a negative combined component count";
|
||||
EXPECT_GE(reported, kCombinedFloor) << "pname 0x" << pname << " fell under the GL 4.6 floor";
|
||||
}
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
}
|
||||
|
||||
// An ordinary 64 KiB block size still produces the plain arithmetic, not a saturated value:
|
||||
// saturation must be the ceiling, never the answer.
|
||||
{
|
||||
MG_Backend::DynamicBackendParameters params;
|
||||
params.MaxUniformBlockSize = 65536;
|
||||
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
|
||||
|
||||
GLint reported = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, &reported);
|
||||
// 4096 default-block components + 14 blocks x (65536 / 4) components each.
|
||||
EXPECT_EQ(reported, 4096 + 14 * (65536 / 4));
|
||||
EXPECT_LT(reported, std::numeric_limits<GLint>::max());
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
}
|
||||
|
||||
MG_State::pGLContext.reset();
|
||||
}
|
||||
|
||||
@@ -563,7 +563,12 @@ namespace {
|
||||
// BuildTBuiltInResource does not read it.
|
||||
a.maxComputeWorkGroupInvocations = 128;
|
||||
b.maxComputeWorkGroupInvocations = 2048;
|
||||
// (4) a spread of DynamicBackendParameters fields the front end never reads
|
||||
// (4) a spread of DynamicBackendParameters fields the front end never reads.
|
||||
// MaxTextureImageUnits used to be here and is NOT any more: the GL 4.6 API-surface wave
|
||||
// made BuildTBuiltInResource read it (gl_MaxTextureImageUnits expands from it), so it
|
||||
// moved to TheFrontendFingerprintMovesWithEveryFrontendLimit. That migration is the
|
||||
// third one this helper has survived; check BuildTBuiltInResource before adding a field
|
||||
// here.
|
||||
a.params.MaxColorTextureSamples = 1;
|
||||
b.params.MaxColorTextureSamples = 8;
|
||||
a.params.MaxTextureSize = 4096;
|
||||
@@ -572,8 +577,8 @@ namespace {
|
||||
b.params.MaxViewports = 16;
|
||||
a.params.MaxUniformBufferBindings = 24;
|
||||
b.params.MaxUniformBufferBindings = 84;
|
||||
a.params.MaxTextureImageUnits = 16;
|
||||
b.params.MaxTextureImageUnits = 32;
|
||||
a.params.MaxRenderbufferSize = 4096;
|
||||
b.params.MaxRenderbufferSize = 16384;
|
||||
return {a, b};
|
||||
}
|
||||
} // namespace
|
||||
@@ -677,6 +682,20 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit)
|
||||
// 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; }},
|
||||
// The GL 4.6 API-surface wave: six more TBuiltInResource fields that used to be stock
|
||||
// glslang literals. The cull pair is the MaxClipDistances story exactly (parse gate plus
|
||||
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances); the texture-image-unit three
|
||||
// and MaxSamples are baked constants (gl_MaxTextureImageUnits,
|
||||
// gl_MaxVertexTextureImageUnits, gl_MaxCombinedTextureImageUnits, gl_MaxSamples - the
|
||||
// last of which also sizes gl_SampleMask[]).
|
||||
{"params.MaxCullDistances", [](CompileEnv& e) { e.params.MaxCullDistances += 1; }},
|
||||
{"params.MaxCombinedClipAndCullDistances",
|
||||
[](CompileEnv& e) { e.params.MaxCombinedClipAndCullDistances += 1; }},
|
||||
{"params.MaxTextureImageUnits", [](CompileEnv& e) { e.params.MaxTextureImageUnits += 1; }},
|
||||
{"params.MaxVertexTextureImageUnits", [](CompileEnv& e) { e.params.MaxVertexTextureImageUnits += 1; }},
|
||||
{"params.MaxCombinedTextureImageUnits",
|
||||
[](CompileEnv& e) { e.params.MaxCombinedTextureImageUnits += 1; }},
|
||||
{"params.MaxSamples", [](CompileEnv& e) { e.params.MaxSamples += 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; }},
|
||||
|
||||
@@ -536,10 +536,15 @@ void main() { g_color = vec4(1); }
|
||||
[] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
|
||||
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
|
||||
GL_INVALID_ENUM},
|
||||
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION
|
||||
// that used to win is still raised for the calls it is actually about.
|
||||
// A mode the enum check accepts falls through to the no-program path, which is now
|
||||
// a SILENT drop rather than an error: GL 4.6 core 7.3 and ES 3.1 7.3 both make a draw
|
||||
// with no current program and no bound pipeline UNDEFINED, not erroneous, and
|
||||
// es31cSeparateShaderObjsTests.StateInteraction reads glGetError() straight after
|
||||
// useProgram(0) + bindProgramPipeline(0) + glDrawElements and requires GL_NO_ERROR.
|
||||
// Dropping the draw is one of the shapes "undefined" may take; inventing an error is
|
||||
// not. The enum check above still outranks it, which is what this case is really for.
|
||||
{"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); },
|
||||
GL_INVALID_OPERATION},
|
||||
GL_NO_ERROR},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
@@ -632,3 +635,394 @@ TEST_F(RenderStateTest, TheFirstScissorWriteBumpsTheVersionEvenWhenTheValueDoesN
|
||||
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
|
||||
EXPECT_EQ(indexed.GetVersion(), indexedSettled);
|
||||
}
|
||||
|
||||
// --- glPatchParameterfv (GL 4.6 core 11.2.2) ---------------------------------------------------
|
||||
//
|
||||
// GL_PATCH_DEFAULT_OUTER_LEVEL / GL_PATCH_DEFAULT_INNER_LEVEL are the tessellation levels a
|
||||
// program with an evaluation stage and NO control stage runs at. glPatchParameterfv was a stub
|
||||
// that stored nothing and raised nothing, so the state could never move off its 1.0 default and
|
||||
// both backends hardcoded 1.0 into the pass-through control stage they synthesize. The getters
|
||||
// were absent too, which is what KHR-GL4x.tessellation_shader.single.
|
||||
// default_values_of_context_wide_properties dies on.
|
||||
|
||||
TEST_F(RenderStateTest, PatchDefaultLevelsStartAtTheGLDefault) {
|
||||
GLfloat outer[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
for (const GLfloat level : outer) EXPECT_FLOAT_EQ(level, 1.0f);
|
||||
|
||||
GLfloat inner[2] = {-1.0f, -1.0f};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
for (const GLfloat level : inner) EXPECT_FLOAT_EQ(level, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, PatchDefaultLevelsRoundTripThroughEveryGetter) {
|
||||
const GLfloat outerIn[4] = {2.0f, 3.5f, 4.0f, 5.25f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerIn);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
const GLfloat innerIn[2] = {6.5f, 7.0f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, innerIn);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLfloat outer[4] = {};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
|
||||
EXPECT_FLOAT_EQ(outer[0], 2.0f);
|
||||
EXPECT_FLOAT_EQ(outer[1], 3.5f);
|
||||
EXPECT_FLOAT_EQ(outer[2], 4.0f);
|
||||
EXPECT_FLOAT_EQ(outer[3], 5.25f);
|
||||
GLfloat inner[2] = {};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
|
||||
EXPECT_FLOAT_EQ(inner[0], 6.5f);
|
||||
EXPECT_FLOAT_EQ(inner[1], 7.0f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// Float state read through the integer and boolean getters: glGetIntegerv rounds (GL 4.6 core
|
||||
// 2.2.2) and glGetBooleanv delegates to it, so both must ANSWER rather than report
|
||||
// INVALID_ENUM - which is exactly what the conformance suite asks them first.
|
||||
GLint outerInts[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerInts);
|
||||
EXPECT_EQ(outerInts[0], 2);
|
||||
EXPECT_EQ(outerInts[1], 4) << "3.5 rounds away from zero";
|
||||
EXPECT_EQ(outerInts[3], 5);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLboolean outerBools[4] = {};
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outerBools);
|
||||
EXPECT_EQ(outerBools[0], GL_TRUE);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLdouble outerDoubles[4] = {};
|
||||
MG_Impl::GLImpl::GetDoublev(GL_PATCH_DEFAULT_OUTER_LEVEL, outerDoubles);
|
||||
EXPECT_DOUBLE_EQ(outerDoubles[3], 5.25) << "glGetDoublev must widen all four, not just the first";
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// glGetInteger64v shares glGetIntegerv's accepted-pname set (GL 4.6 core 22.1), so it owes the
|
||||
// same component count. Its own table listed neither pname, so three of the four words were
|
||||
// left holding whatever the caller's buffer held - and no error said so.
|
||||
GLint64 outerLongs[4] = {9, 9, 9, 9};
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_OUTER_LEVEL, outerLongs);
|
||||
EXPECT_EQ(outerLongs[0], 2);
|
||||
EXPECT_EQ(outerLongs[3], 5) << "glGetInteger64v must write all four, not just the first";
|
||||
GLint64 innerLongs[2] = {9, 9};
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_PATCH_DEFAULT_INNER_LEVEL, innerLongs);
|
||||
EXPECT_EQ(innerLongs[1], 7);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// Put the context back where the rest of the binary expects it.
|
||||
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
const GLfloat defaults2[2] = {1.0f, 1.0f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// GL 4.6 core 2.2.2: a float state comes back through glGetBooleanv as GL_FALSE only when it is
|
||||
// zero. Deriving the answer from glGetIntegerv - which rounds - reported GL_FALSE for a level of
|
||||
// 0.25, which is neither zero nor anything the application asked to be rounded.
|
||||
TEST_F(RenderStateTest, PatchDefaultLevelsBelowHalfAreStillTrueAsBooleans) {
|
||||
const GLfloat fractional[4] = {0.25f, 0.0f, 0.4f, 0.25f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, fractional);
|
||||
const GLfloat fractionalInner[2] = {0.25f, 0.0f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, fractionalInner);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLboolean outer[4] = {};
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_OUTER_LEVEL, outer);
|
||||
EXPECT_EQ(outer[0], GL_TRUE) << "0.25 is not zero";
|
||||
EXPECT_EQ(outer[1], GL_FALSE) << "0.0 is the one value that is false";
|
||||
EXPECT_EQ(outer[2], GL_TRUE);
|
||||
GLboolean inner[2] = {};
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_PATCH_DEFAULT_INNER_LEVEL, inner);
|
||||
EXPECT_EQ(inner[0], GL_TRUE);
|
||||
EXPECT_EQ(inner[1], GL_FALSE);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const GLfloat defaults4[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
const GLfloat defaults2[2] = {1.0f, 1.0f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL, defaults4);
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL, defaults2);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, PatchParameterfvRejectsEveryOtherPname) {
|
||||
const GLfloat levels[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_PATCH_VERTICES, levels);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
MG_Impl::GLImpl::PatchParameterfv(GL_MAX_PATCH_VERTICES, levels);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
// The integer setter keeps its own, disjoint, accepted pname.
|
||||
MG_Impl::GLImpl::PatchParameteri(GL_PATCH_DEFAULT_OUTER_LEVEL, 4);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, PatchDefaultLevelsAreTreatedAsPipelineState) {
|
||||
// Load-bearing: both backends compile these numbers into the pass-through tessellation control
|
||||
// stage they synthesize, so a change has to invalidate an already-built program the same way a
|
||||
// glPatchParameteri does. Bumping only the all-state version would leave DirectVulkan's
|
||||
// pipeline memo - which keys on the PIPELINE-state version - handing back a pipeline built
|
||||
// with the old levels.
|
||||
MG_State::GLState::RenderState state;
|
||||
const Uint initialPipelineVersion = state.GetPipelineStateVersion();
|
||||
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
|
||||
EXPECT_GT(state.GetPipelineStateVersion(), initialPipelineVersion);
|
||||
|
||||
const Uint settled = state.GetPipelineStateVersion();
|
||||
state.SetPatchDefaultOuterLevel(FloatVec4(2.0f, 2.0f, 2.0f, 2.0f));
|
||||
EXPECT_EQ(state.GetPipelineStateVersion(), settled) << "a redundant write is free";
|
||||
|
||||
state.SetPatchDefaultInnerLevel(FloatVec2(3.0f, 3.0f));
|
||||
EXPECT_GT(state.GetPipelineStateVersion(), settled);
|
||||
}
|
||||
|
||||
// glPatchParameterfv accepts NaN by design, and NaN is never equal to itself under IEEE `==`. A
|
||||
// value-compared redundant-write guard therefore never settles: every re-set of the identical
|
||||
// tuple bumps the pipeline-state version, and - one level down - DirectGLES's staleness clause
|
||||
// re-transpiles, re-compiles and re-links the synthesized pass-through stage on every draw. Both
|
||||
// compare BIT PATTERNS instead, which is what DirectVulkan's module key already hashes.
|
||||
TEST_F(RenderStateTest, ARedundantNaNPatchLevelWriteSettlesInsteadOfBumpingForever) {
|
||||
const Float notANumber = std::numeric_limits<Float>::quiet_NaN();
|
||||
MG_State::GLState::RenderState state;
|
||||
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
|
||||
const Uint afterFirst = state.GetPipelineStateVersion();
|
||||
|
||||
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 1.0f, 1.0f, 1.0f));
|
||||
EXPECT_EQ(state.GetPipelineStateVersion(), afterFirst)
|
||||
<< "the identical NaN tuple is not a state change";
|
||||
|
||||
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
|
||||
const Uint afterInner = state.GetPipelineStateVersion();
|
||||
state.SetPatchDefaultInnerLevel(FloatVec2(notANumber, 1.0f));
|
||||
EXPECT_EQ(state.GetPipelineStateVersion(), afterInner);
|
||||
|
||||
// A genuinely different tuple still moves, so the guard has not simply gone blind.
|
||||
state.SetPatchDefaultOuterLevel(FloatVec4(notANumber, 2.0f, 1.0f, 1.0f));
|
||||
EXPECT_GT(state.GetPipelineStateVersion(), afterInner);
|
||||
}
|
||||
|
||||
// --- desktop GL_PRIMITIVE_RESTART state --------------------------------------------------------
|
||||
//
|
||||
// The cap and its index are what a desktop application enables instead of ES's
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX. Both halves have to be answerable, because the backends read
|
||||
// them on every indexed draw to decide whether the index data needs rewriting.
|
||||
TEST_F(RenderStateTest, PrimitiveRestartCapAndIndexAreBothQueryable) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_FALSE);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_PRIMITIVE_RESTART);
|
||||
MG_Impl::GLImpl::PrimitiveRestartIndex(1026u);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART), GL_TRUE);
|
||||
GLint index = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_PRIMITIVE_RESTART_INDEX, &index);
|
||||
EXPECT_EQ(index, 1026);
|
||||
// The fixed-index cap is a separate piece of state and must not have moved.
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_PRIMITIVE_RESTART_FIXED_INDEX), GL_FALSE);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_PRIMITIVE_RESTART);
|
||||
MG_Impl::GLImpl::PrimitiveRestartIndex(0u);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
// glMinSampleShading was a logging no-op while ARB_sample_shading was advertised and
|
||||
// glEnable(GL_SAMPLE_SHADING) fell through RenderState::SetCapability's default arm, so an
|
||||
// application could turn sample shading on, ask for a rate, and get neither - with every query
|
||||
// agreeing that nothing had happened.
|
||||
TEST_F(RenderStateTest, MinSampleShadingRoundTripsAndClamps) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
// GL 4.6 core table 23.10: the initial value is 0.
|
||||
GLfloat initial = -1.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &initial);
|
||||
EXPECT_FLOAT_EQ(initial, 0.0f);
|
||||
|
||||
MG_Impl::GLImpl::MinSampleShading(0.25f);
|
||||
GLfloat value = -1.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
|
||||
EXPECT_FLOAT_EQ(value, 0.25f);
|
||||
|
||||
// The fraction survives the double query too, and rounds - not truncates - for the integer one.
|
||||
GLdouble asDouble = -1.0;
|
||||
MG_Impl::GLImpl::GetDoublev(GL_MIN_SAMPLE_SHADING_VALUE, &asDouble);
|
||||
EXPECT_NEAR(asDouble, 0.25, 1e-6);
|
||||
GLint asInt = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MIN_SAMPLE_SHADING_VALUE, &asInt);
|
||||
EXPECT_EQ(asInt, 0);
|
||||
// A non-zero fraction is GL_TRUE, which the integer path would have rounded away first.
|
||||
GLboolean asBoolean = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_MIN_SAMPLE_SHADING_VALUE, &asBoolean);
|
||||
EXPECT_EQ(asBoolean, GL_TRUE);
|
||||
|
||||
// "value is clamped to [0, 1]" - not an error, a clamp.
|
||||
MG_Impl::GLImpl::MinSampleShading(2.0f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
|
||||
EXPECT_FLOAT_EQ(value, 1.0f);
|
||||
MG_Impl::GLImpl::MinSampleShading(-3.0f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_MIN_SAMPLE_SHADING_VALUE, &value);
|
||||
EXPECT_FLOAT_EQ(value, 0.0f);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::MinSampleShading(0.0f);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, SampleShadingEnableIsStoredAndQueryable) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
|
||||
|
||||
MG_Impl::GLImpl::Enable(GL_SAMPLE_SHADING);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_TRUE);
|
||||
GLboolean asBoolean = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_SAMPLE_SHADING, &asBoolean);
|
||||
EXPECT_EQ(asBoolean, GL_TRUE);
|
||||
GLint asInt = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_SAMPLE_SHADING, &asInt);
|
||||
EXPECT_EQ(asInt, GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_SAMPLE_SHADING);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SAMPLE_SHADING), GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// glClipControl and glPolygonOffsetClamp were DECLARE_GL_FUNCTION_STUB_HEAD entry points: they
|
||||
// took their arguments, recorded nothing and raised no error, and the state variables they own
|
||||
// (GL_CLIP_ORIGIN, GL_CLIP_DEPTH_MODE, GL_POLYGON_OFFSET_CLAMP) had no arm in any getter, so the
|
||||
// very first query of a conformance case raised GL_INVALID_ENUM and killed it. These assertions
|
||||
// are state-shaped on purpose - the rasterization half of clip control is a backend question, but
|
||||
// the state machine has to round-trip regardless of what a backend does with it.
|
||||
TEST_F(RenderStateTest, ClipControlStateRoundTripsAndDefaultsToLowerLeftNegativeOneToOne) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLint origin = 0;
|
||||
GLint depthMode = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
|
||||
EXPECT_EQ(origin, GL_LOWER_LEFT);
|
||||
EXPECT_EQ(depthMode, GL_NEGATIVE_ONE_TO_ONE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
|
||||
EXPECT_EQ(origin, GL_UPPER_LEFT);
|
||||
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// Every getter flavour has to answer, not just the integer one - the conformance suite reads
|
||||
// this state through all of them.
|
||||
GLfloat asFloat = 0.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_CLIP_ORIGIN, &asFloat);
|
||||
EXPECT_EQ(static_cast<GLint>(asFloat), GL_UPPER_LEFT);
|
||||
GLint64 asInt64 = 0;
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_CLIP_DEPTH_MODE, &asInt64);
|
||||
EXPECT_EQ(static_cast<GLint>(asInt64), GL_ZERO_TO_ONE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ClipControlRejectsBadEnumsAndLeavesTheStateAlone) {
|
||||
DrainPendingGlErrors();
|
||||
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_ZERO_TO_ONE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
MG_Impl::GLImpl::ClipControl(GL_FRONT, GL_ZERO_TO_ONE);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
MG_Impl::GLImpl::ClipControl(GL_UPPER_LEFT, GL_FRONT);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
GLint origin = 0;
|
||||
GLint depthMode = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_ORIGIN, &origin);
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_CLIP_DEPTH_MODE, &depthMode);
|
||||
EXPECT_EQ(origin, GL_UPPER_LEFT) << "a rejected glClipControl must not change the state";
|
||||
EXPECT_EQ(depthMode, GL_ZERO_TO_ONE) << "a rejected glClipControl must not change the state";
|
||||
|
||||
MG_Impl::GLImpl::ClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, PolygonOffsetClampStoresTheClampAndTheFactorUnitsPair) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLfloat clamp = -1.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
|
||||
EXPECT_FLOAT_EQ(clamp, 0.0f) << "the default clamp is zero, i.e. no clamping";
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::PolygonOffsetClamp(1.5f, 2.5f, 0.5f);
|
||||
GLfloat factor = 0.0f;
|
||||
GLfloat units = 0.0f;
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
|
||||
EXPECT_FLOAT_EQ(factor, 1.5f);
|
||||
EXPECT_FLOAT_EQ(units, 2.5f);
|
||||
EXPECT_FLOAT_EQ(clamp, 0.5f) << "the fractional clamp must survive - the integer path rounds it away";
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// glcPolygonOffsetClampTests reads GL_POLYGON_OFFSET_CLAMP through all five getters and
|
||||
// requires no error from any of them; that is what used to kill the availability case.
|
||||
GLboolean asBoolean = GL_FALSE;
|
||||
MG_Impl::GLImpl::GetBooleanv(GL_POLYGON_OFFSET_CLAMP, &asBoolean);
|
||||
EXPECT_EQ(asBoolean, GL_TRUE);
|
||||
GLint asInt = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_POLYGON_OFFSET_CLAMP, &asInt);
|
||||
EXPECT_EQ(asInt, 1) << "0.5 rounds to nearest for the integer query";
|
||||
GLint64 asInt64 = -1;
|
||||
MG_Impl::GLImpl::GetInteger64v(GL_POLYGON_OFFSET_CLAMP, &asInt64);
|
||||
EXPECT_EQ(asInt64, 1);
|
||||
GLdouble asDouble = -1.0;
|
||||
MG_Impl::GLImpl::GetDoublev(GL_POLYGON_OFFSET_CLAMP, &asDouble);
|
||||
EXPECT_NEAR(asDouble, 0.5, 1e-6);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// GL 4.6 core 14.6.5 defines glPolygonOffset(factor, units) as EQUIVALENT to
|
||||
// glPolygonOffsetClamp(factor, units, 0) - totally, not "except for the clamp". So it writes
|
||||
// all three, and a clamp left over from an earlier glPolygonOffsetClamp must be gone.
|
||||
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
|
||||
EXPECT_FLOAT_EQ(factor, 3.0f);
|
||||
EXPECT_FLOAT_EQ(units, 4.0f);
|
||||
EXPECT_FLOAT_EQ(clamp, 0.0f) << "glPolygonOffset IS PolygonOffsetClamp(factor, units, 0)";
|
||||
|
||||
// The same rule when factor and units do NOT change: the clamp still has to be cleared, which
|
||||
// an early-out keyed on the factor/units pair alone would skip.
|
||||
MG_Impl::GLImpl::PolygonOffsetClamp(3.0f, 4.0f, 0.75f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
|
||||
ASSERT_FLOAT_EQ(clamp, 0.75f);
|
||||
MG_Impl::GLImpl::PolygonOffset(3.0f, 4.0f);
|
||||
MG_Impl::GLImpl::GetFloatv(GL_POLYGON_OFFSET_CLAMP, &clamp);
|
||||
EXPECT_FLOAT_EQ(clamp, 0.0f) << "a no-op factor/units write must still clear the clamp";
|
||||
|
||||
MG_Impl::GLImpl::PolygonOffsetClamp(0.0f, 0.0f, 0.0f);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_TEXTURE_BUFFER_BINDING (0x8C2A) is the same token as GL_TEXTURE_BUFFER; as a glGetIntegerv
|
||||
// pname it asks which BUFFER object is bound there, and it had no arm at all, so
|
||||
// esextcTextureBufferParameters died on its first query.
|
||||
TEST_F(RenderStateTest, TextureBufferBindingAnswersTheBoundBufferName) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLint binding = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BUFFER_BINDING, &binding);
|
||||
EXPECT_EQ(binding, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// GL_ARB_spirv_extensions. Zero is legal and true: MobileGL relies on no SPIR-V extension, so
|
||||
// glGetStringi(GL_SPIR_V_EXTENSIONS, i) is never legally reached.
|
||||
TEST_F(RenderStateTest, NumSpirVExtensionsIsQueryableAndZero) {
|
||||
DrainPendingGlErrors();
|
||||
|
||||
GLint count = -1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_NUM_SPIR_V_EXTENSIONS, &count);
|
||||
EXPECT_EQ(count, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
#include "Init.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
@@ -190,6 +191,36 @@ namespace {
|
||||
EXPECT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 3);
|
||||
}
|
||||
|
||||
// ...and the level a FRAMEBUFFER may attach is the view's own count, not the inherited
|
||||
// TEXTURE_IMMUTABLE_LEVELS the test above pins. Bounding glFramebufferTexture by the latter
|
||||
// accepted a level the view cannot reach, which attaches a 0x0 image: the framebuffer then
|
||||
// reports COMPLETE and nothing can be drawn into it.
|
||||
TEST_F(TextureViewTest, AFramebufferAttachIsBoundedByTheViewsOwnLevelCount) {
|
||||
const GLuint storage = MakeImmutable2D(4, 32, 32);
|
||||
const GLuint view = GenTexture();
|
||||
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, /*minlevel=*/2,
|
||||
/*numlevels=*/2, 0, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
// The inherited query really does report the original's four levels...
|
||||
ASSERT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS), 4);
|
||||
// ...while the view itself has two.
|
||||
ASSERT_EQ(GetViewParameter(view, GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS), 2);
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, view, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, view, 2);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
DrainPendingGlErrors();
|
||||
}
|
||||
|
||||
TEST_F(TextureViewTest, ViewClampsItsLevelCountToWhatRemains) {
|
||||
const GLuint storage = MakeImmutable2D(3, 16, 16);
|
||||
const GLuint view = GenTexture();
|
||||
|
||||
@@ -1139,6 +1139,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// optimistic 8 behind, so the frontend promised eight clip planes and every draw with a
|
||||
// clipping program silently rendered nothing. The guarded probe below only ever widens it.
|
||||
GLint maxClipDistances = 0;
|
||||
// The cull half of the same extension, and the same "zero is a legal answer" rule: a cull
|
||||
// distance discards the whole primitive, so promising eight on a driver that has none does
|
||||
// not fail loudly, it drops every draw of a culling program.
|
||||
GLint maxCullDistances = 0;
|
||||
GLint maxCombinedClipAndCullDistances = 0;
|
||||
GLint maxViewports = 16;
|
||||
// GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal
|
||||
// answer rather than a placeholder: with neither geometry shaders nor a viewport array
|
||||
@@ -1333,6 +1338,23 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
"rejected; reporting no clip distances");
|
||||
maxClipDistances = 0;
|
||||
}
|
||||
// GL_MAX_CULL_DISTANCES_EXT (0x82F9) and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT
|
||||
// (0x82FA) are the same tokens as their desktop spellings and arrive with the same
|
||||
// extension, so they are probed under the same guard and the same drain sandwich.
|
||||
drainErrors();
|
||||
glesFuncs.glGetIntegerv(GL_MAX_CULL_DISTANCES, &maxCullDistances);
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CULL_DISTANCES was "
|
||||
"rejected; reporting no cull distances");
|
||||
maxCullDistances = 0;
|
||||
}
|
||||
drainErrors();
|
||||
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &maxCombinedClipAndCullDistances);
|
||||
if (drainErrors()) {
|
||||
MGLOG_W("GL_EXT_clip_cull_distance is advertised but "
|
||||
"GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES was rejected; deriving it from the pair");
|
||||
maxCombinedClipAndCullDistances = 0;
|
||||
}
|
||||
}
|
||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
|
||||
@@ -1562,6 +1584,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// A driver is free to write nonsense into an out-param it then rejects, and without the
|
||||
// extension the probe above never ran at all - so the flag, not the local, decides.
|
||||
caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0;
|
||||
caps.MaxCullDistances = caps.SupportsClipDistance ? std::max(maxCullDistances, 0) : 0;
|
||||
// The combined limit can never be smaller than either half (GL 4.6 core 11.1.3.10 / the
|
||||
// EXT spec say so), so a driver that rejected the combined query but answered the other
|
||||
// two still gets a usable - and never over-stated - number.
|
||||
caps.MaxCombinedClipAndCullDistances =
|
||||
caps.SupportsClipDistance
|
||||
? std::max({maxCombinedClipAndCullDistances, caps.MaxClipDistances, caps.MaxCullDistances})
|
||||
: 0;
|
||||
caps.MaxViewports = maxViewports;
|
||||
caps.LayerProvokingVertex = layerProvokingVertex;
|
||||
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
|
||||
@@ -1651,6 +1681,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
// and "this driver has no clip distances".
|
||||
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances,
|
||||
caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)");
|
||||
MGLOG_I(" GL_MAX_CULL_DISTANCES: %d", caps.MaxCullDistances);
|
||||
MGLOG_I(" GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES: %d", caps.MaxCombinedClipAndCullDistances);
|
||||
MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports);
|
||||
MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
|
||||
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
|
||||
|
||||
@@ -1299,6 +1299,11 @@ namespace MobileGL {
|
||||
// GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe
|
||||
// in FillInGLESCapabilities.
|
||||
Int MaxClipDistances = 0;
|
||||
// Same contract, same reason, same extension: GL_MAX_CULL_DISTANCES_EXT and
|
||||
// GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT exist in ES only under
|
||||
// GL_EXT_clip_cull_distance, so zero is the honest answer without it.
|
||||
Int MaxCullDistances = 0;
|
||||
Int MaxCombinedClipAndCullDistances = 0;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX
|
||||
// (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what
|
||||
|
||||
@@ -199,6 +199,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments);
|
||||
caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments);
|
||||
caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances);
|
||||
caps.MaxCullDistances = SaturateToInt(p.limits.maxCullDistances);
|
||||
caps.MaxCombinedClipAndCullDistances = SaturateToInt(p.limits.maxCombinedClipAndCullDistances);
|
||||
caps.MaxViewports = SaturateToInt(p.limits.maxViewports);
|
||||
caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]);
|
||||
caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]);
|
||||
@@ -239,6 +241,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
||||
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
|
||||
caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE;
|
||||
caps.SupportsShaderCullDistance = supportedFeatures.shaderCullDistance == VK_TRUE;
|
||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
||||
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
||||
HasUsableShaderSubgroupSupport(subgroupProps);
|
||||
@@ -319,6 +322,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments);
|
||||
caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments);
|
||||
caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances);
|
||||
caps.MaxCullDistances = SaturateToInt(properties.limits.maxCullDistances);
|
||||
caps.MaxCombinedClipAndCullDistances = SaturateToInt(properties.limits.maxCombinedClipAndCullDistances);
|
||||
caps.MaxViewports = SaturateToInt(properties.limits.maxViewports);
|
||||
caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]);
|
||||
caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]);
|
||||
@@ -336,6 +341,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.SupportsFragmentStoresAndAtomics = false;
|
||||
caps.SupportsGeometryShader = false;
|
||||
caps.SupportsShaderClipDistance = false;
|
||||
caps.SupportsShaderCullDistance = false;
|
||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
||||
caps.SupportsShaderSubgroup = false;
|
||||
caps.SubgroupSize = 0;
|
||||
|
||||
@@ -68,6 +68,11 @@ namespace MobileGL {
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
Int MaxClipDistances = 8;
|
||||
// VkPhysicalDeviceLimits::maxCullDistances / maxCombinedClipAndCullDistances, gated
|
||||
// by SupportsShaderCullDistance exactly as the clip pair is gated by
|
||||
// SupportsShaderClipDistance.
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
Int MaxViewportWidth = 16384;
|
||||
Int MaxViewportHeight = 16384;
|
||||
@@ -106,6 +111,11 @@ namespace MobileGL {
|
||||
// device has it, and without it a shader writing gl_ClipDistance is invalid. Very
|
||||
// widely supported, hence read from the device features and never assumed false.
|
||||
Bool SupportsShaderClipDistance = false;
|
||||
// VkPhysicalDeviceFeatures::shaderCullDistance, the same story one field down:
|
||||
// VulkanRenderer already ENABLES this feature where the device has it, but nobody
|
||||
// ever read the limits it unlocks, so the frontend advertised eight cull distances
|
||||
// from a literal instead of from the device.
|
||||
Bool SupportsShaderCullDistance = false;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Bool SupportsShaderSubgroup = false;
|
||||
Uint32 SubgroupSize = 0;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// MobileGL - MobileGL/MG_Util/Math/FixedPointConversion.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace MobileGL::MG_Util {
|
||||
// GL 4.6 core 2.3.5 "Fixed-Point Data Conversions", for the 32-bit signed normalized pair that
|
||||
// GL_TEXTURE_BORDER_COLOR is specified and queried in when the NON-"I" integer entry points are
|
||||
// used (glTexParameteriv / glSamplerParameteriv / glGetTexParameteriv / glGetSamplerParameteriv).
|
||||
// The "I" entry points (TexParameterIiv / Iuiv) carry a raw integer border colour instead and
|
||||
// must NOT go through these.
|
||||
//
|
||||
// The two directions have to be an exact pair or a legal round trip is destroyed: the CTS writes
|
||||
// {0,1,2,4} with glTexParameteriv and demands {0,1,2,4} back from glGetTexParameteriv. Reading
|
||||
// with a bare static_cast<GLint> (which is what the truncating read used to do) answers {0,0,0,0}
|
||||
// because equation 2.2 has already scaled those integers down to ~1e-9.
|
||||
//
|
||||
// b = 32, so the scale is 2^31 - 1 = 2147483647. It is held in DOUBLE deliberately: as a binary32
|
||||
// it rounds up to 2^31, and the inverse direction would then answer -2147483648 for f = -1.0
|
||||
// where the equation says -2147483647. The forward direction is unaffected either way (a small
|
||||
// integer divided by 2147483647 lands on the same float as one divided by 2^31), so one exact
|
||||
// constant serves both and the pair stays a true inverse: c -> c/(2^31-1) -> c.
|
||||
inline constexpr double kSignedNormalizedInt32Scale = 2147483647.0;
|
||||
|
||||
// Equation 2.2: c / (2^(b-1) - 1), clamped below at -1 so the extra negative code (-2^31) does
|
||||
// not produce a value outside [-1, 1].
|
||||
inline Float SignedNormalizedInt32ToFloat(Int32 value) {
|
||||
return std::max(static_cast<Float>(static_cast<double>(value) / kSignedNormalizedInt32Scale), -1.0f);
|
||||
}
|
||||
|
||||
// Equation 2.3: round(f * (2^(b-1) - 1)). f is clamped to [-1, 1] first, as the equation's domain
|
||||
// requires; the multiply is done in double so a near-1 float cannot round past INT32_MAX before
|
||||
// the cast, which is undefined behaviour rather than a saturating one.
|
||||
inline Int32 FloatToSignedNormalizedInt32(Float value) {
|
||||
if (std::isnan(value)) return 0;
|
||||
const Float clamped = std::clamp(value, -1.0f, 1.0f);
|
||||
const double scaled = std::round(static_cast<double>(clamped) * kSignedNormalizedInt32Scale);
|
||||
return static_cast<Int32>(std::clamp(scaled, -2147483648.0, 2147483647.0));
|
||||
}
|
||||
} // namespace MobileGL::MG_Util
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL {
|
||||
template <typename Derived, typename T, SizeT N>
|
||||
struct VecBase {
|
||||
@@ -84,6 +86,16 @@ namespace MobileGL {
|
||||
}
|
||||
};
|
||||
|
||||
// Bit-pattern equality, for a vector used as a cache or staleness KEY rather than as a
|
||||
// number. IEEE `==` - which operator== above is - says a NaN never equals itself, so a single
|
||||
// NaN component makes every comparison answer "changed" and whatever the key guards is
|
||||
// rebuilt on every use, forever. Two zeros of opposite sign compare unequal here, which only
|
||||
// ever costs one extra rebuild.
|
||||
template <typename Derived, typename T, SizeT N>
|
||||
Bool BitwiseEqual(const VecBase<Derived, T, N>& a, const VecBase<Derived, T, N>& b) {
|
||||
return std::memcmp(a.data.data(), b.data.data(), sizeof(T) * N) == 0;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct Vec2 : public VecBase<Vec2<T>, T, 2> {
|
||||
using Base = VecBase<Vec2<T>, T, 2>;
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
Uint32 FixedRestartIndexForGLType(GLenum indexType) {
|
||||
switch (indexType) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
case GL_UNSIGNED_INT: return 0xFFFFFFFFu;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
SizeT GetGLTypeSize(GLenum type) {
|
||||
switch (type) {
|
||||
// Scalars
|
||||
|
||||
@@ -12,5 +12,19 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
SizeT GetGLTypeSize(GLenum type);
|
||||
|
||||
// The largest value an index of this type can hold, which is also the value
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX (and its GLES/Vulkan equivalents) restart on. Zero for a
|
||||
// type that cannot index at all.
|
||||
//
|
||||
// Shared rather than re-derived per backend on purpose: three places have to agree about
|
||||
// what an index of this type can be - whether a rewrite is needed at all, what the rewrite
|
||||
// compares against, and whether the driver should be told to restart. GL 4.6 core 10.3.6
|
||||
// compares the FETCHED index, zero-extended, against the full 32-bit
|
||||
// PRIMITIVE_RESTART_INDEX, so a restart index greater than this value matches no index and
|
||||
// the draw restarts nowhere. Truncating it to the type's width instead - which one of these
|
||||
// three places used to do - turns a legal vertex index into a restart.
|
||||
Uint32 FixedRestartIndexForGLType(GLenum indexType);
|
||||
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -23,6 +23,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Int ResolveMaxVertexAttribs(const Bool hasBackend, const Int backendMaxVertexAttribs) {
|
||||
constexpr Int capacity = static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
if (!hasBackend) return capacity;
|
||||
if (backendMaxVertexAttribs <= 0) return capacity;
|
||||
return std::min(backendMaxVertexAttribs, capacity);
|
||||
}
|
||||
|
||||
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) {
|
||||
Uint64 state = 0x9e3779b97f4a7c15ull;
|
||||
HashValue(state, env.maxComputeWorkGroupSize[0]);
|
||||
@@ -67,6 +74,24 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// and expands gl_MaxClipDistances from the same number, so it decides both whether a
|
||||
// shader compiles at all and what a module that reads the constant generates.
|
||||
HashValue(state, env.params.MaxClipDistances);
|
||||
// The cull-distance pair, added when the GL 4.6 API-surface wave made them env-derived:
|
||||
// they were bare literals (8/8) in BuildTBuiltInResource while no backend had ever been
|
||||
// asked whether it can host a cull distance. Exactly the MaxClipDistances class - glslang
|
||||
// bounds gl_CullDistance[i] against maxCullDistances at parse and expands
|
||||
// gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances from the same numbers.
|
||||
HashValue(state, env.params.MaxCullDistances);
|
||||
HashValue(state, env.params.MaxCombinedClipAndCullDistances);
|
||||
// The texture-image-unit family, made env-derived in the same wave. They were stock
|
||||
// glslang defaults (32/32/80) that disagreed with what glGetIntegerv answered, and
|
||||
// gl_MaxTextureImageUnits / gl_MaxVertexTextureImageUnits / gl_MaxCombinedTextureImageUnits
|
||||
// expand from them.
|
||||
HashValue(state, env.params.MaxTextureImageUnits);
|
||||
HashValue(state, env.params.MaxVertexTextureImageUnits);
|
||||
HashValue(state, env.params.MaxCombinedTextureImageUnits);
|
||||
// gl_MaxSamples, which also sizes gl_SampleMask[] / gl_SampleMaskIn[] and bounds a
|
||||
// constant index into them, so a module that touches either generates different SPIR-V
|
||||
// on two backends that report different sample counts.
|
||||
HashValue(state, env.params.MaxSamples);
|
||||
// The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be
|
||||
// hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many
|
||||
// words that the day they became backend-derived they would have to move in here -
|
||||
|
||||
@@ -84,11 +84,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
|
||||
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
|
||||
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
|
||||
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. glslang
|
||||
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances,
|
||||
// MaxCullDistances, MaxCombinedClipAndCullDistances, MaxTextureImageUnits,
|
||||
// MaxVertexTextureImageUnits, MaxCombinedTextureImageUnits, MaxSamples. glslang
|
||||
// enforces those at parse, so they decide whether a shader compiles at all and can
|
||||
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d),
|
||||
// the third time in three waves that a hardcoded TBuiltInResource field became
|
||||
// env-derived - assume the next wave does it again and re-audit.
|
||||
// change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d)
|
||||
// and the six after it at the GL 4.6 API-surface wave - the fourth time in four
|
||||
// waves that a hardcoded TBuiltInResource field became env-derived. Assume the next
|
||||
// wave does it again and re-audit.
|
||||
// * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each.
|
||||
// These moved IN at the dev merge that brought wave3's cb155c5b, which made
|
||||
// BuildTBuiltInResource read them from the env instead of hardcoding a permissive
|
||||
@@ -160,6 +163,18 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
}
|
||||
};
|
||||
|
||||
// How many vertex input locations exist, from the frontend's point of view: the backend's
|
||||
// advertised count bounded by the state layer's current-value storage capacity
|
||||
// (VertexArrayObject::MAX_VERTEX_ATTRIBS). ONE definition, because three places have to
|
||||
// agree on it and used to carry three copies of the formula - glGetIntegerv
|
||||
// (VertexArrayImpl::GetMaxVertexAttribs), the limit reflection records vertex inputs
|
||||
// against (ProgramLinkTask), and gl_MaxVertexAttribs (BuildTBuiltInResource, which had a
|
||||
// hardcoded 64 the other two never saw). A disagreement there is not cosmetic: glslang
|
||||
// ACCEPTS a vertex input at a location the runtime cannot bind, and the draw then silently
|
||||
// reads nothing. `hasBackend` false means "no backend to be bounded by" and yields the
|
||||
// storage capacity, matching what all three did before.
|
||||
Int ResolveMaxVertexAttribs(Bool hasBackend, Int backendMaxVertexAttribs);
|
||||
|
||||
// Hashes every semantically relevant member. Public so a test can assert that two
|
||||
// different envs really do produce different P0b cache keys.
|
||||
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
|
||||
#include "ShaderCompiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||
@@ -64,6 +69,30 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Above every plausible GL_MAX_TESS_GEN_LEVEL (the GL core minimum is 64), so it lands on the
|
||||
// same clamped result the device's own maximum would. +inf has to reach the tessellator as
|
||||
// "as finely as possible", not as "discard".
|
||||
static constexpr const char* kClampedHighTessLevelLiteral = "65536.0";
|
||||
|
||||
String TessellationLevelLiteral(Float value) {
|
||||
// GL leaves a NaN level unspecified; 0.0 is the safe reading, and unlike "nan" it compiles.
|
||||
if (std::isnan(value)) return "0.0";
|
||||
// -inf is <= 0 and discards the patch, exactly like 0.0. +inf clamps to the maximum.
|
||||
if (std::isinf(value)) return value > 0.0f ? kClampedHighTessLevelLiteral : "0.0";
|
||||
|
||||
// Shortest round-trip, not a fixed six decimals: "{:.6f}" renders every level below ~5e-7
|
||||
// as "0.000000", which turns a positive level GL would clamp to 1 into a discarded patch.
|
||||
String text = std::format("{}", value);
|
||||
// ...but shortest round-trip spells an integral value as a bare digit sequence, which GLSL
|
||||
// reads as an INT literal, so the decimal point has to be put back when nothing else marks
|
||||
// the literal as floating point.
|
||||
if (text.find('.') == String::npos && text.find('e') == String::npos &&
|
||||
text.find('E') == String::npos) {
|
||||
text += ".0";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// `env` is the compile-time backend snapshot; null means "resolve from the live
|
||||
// backend", which is what the standalone/test entry points do. The pipeline always
|
||||
// passes one, so a worker never reaches pActiveBackendObject through here.
|
||||
@@ -73,16 +102,11 @@ namespace MobileGL {
|
||||
Resources.maxClipPlanes = 6;
|
||||
Resources.maxTextureUnits = 32;
|
||||
Resources.maxTextureCoords = 32;
|
||||
Resources.maxVertexAttribs = 64;
|
||||
Resources.maxVertexUniformComponents = 4096;
|
||||
Resources.maxVaryingFloats = 64;
|
||||
Resources.maxVertexTextureImageUnits = 32;
|
||||
Resources.maxCombinedTextureImageUnits = 80;
|
||||
Resources.maxTextureImageUnits = 32;
|
||||
Resources.maxVertexUniformComponents = MAX_VERTEX_UNIFORM_COMPONENTS;
|
||||
Resources.maxVaryingFloats = MAX_VARYING_COMPONENTS;
|
||||
Resources.maxFragmentUniformComponents = 4096;
|
||||
Resources.maxDrawBuffers = 32;
|
||||
Resources.maxVertexUniformVectors = 128;
|
||||
Resources.maxVaryingVectors = 8;
|
||||
Resources.maxVertexUniformVectors = MAX_VERTEX_UNIFORM_VECTORS;
|
||||
Resources.maxVaryingVectors = MAX_VARYING_VECTORS;
|
||||
Resources.maxFragmentUniformVectors = 256;
|
||||
Resources.maxVertexOutputVectors = 16;
|
||||
Resources.maxFragmentInputVectors = 15;
|
||||
@@ -93,14 +117,12 @@ namespace MobileGL {
|
||||
Resources.maxComputeImageUniforms = 8;
|
||||
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||
Resources.maxVaryingComponents = 60;
|
||||
Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS;
|
||||
Resources.maxVertexOutputComponents = 64;
|
||||
Resources.maxGeometryInputComponents = 64;
|
||||
Resources.maxGeometryOutputComponents = 128;
|
||||
Resources.maxFragmentInputComponents = 128;
|
||||
Resources.maxImageUnits = 8;
|
||||
Resources.maxCombinedImageUnitsAndFragmentOutputs = 8;
|
||||
Resources.maxCombinedShaderOutputResources = 8;
|
||||
Resources.maxImageSamples = 0;
|
||||
Resources.maxVertexImageUniforms = 0;
|
||||
Resources.maxTessControlImageUniforms = 0;
|
||||
@@ -113,16 +135,18 @@ namespace MobileGL {
|
||||
Resources.maxGeometryTotalOutputComponents = 1024;
|
||||
Resources.maxGeometryUniformComponents = 1024;
|
||||
Resources.maxGeometryVaryingComponents = 64;
|
||||
Resources.maxTessControlInputComponents = 128;
|
||||
Resources.maxTessControlOutputComponents = 128;
|
||||
Resources.maxTessControlTextureImageUnits = 16;
|
||||
Resources.maxTessControlUniformComponents = 1024;
|
||||
Resources.maxTessControlTotalOutputComponents = 4096;
|
||||
Resources.maxTessEvaluationInputComponents = 128;
|
||||
Resources.maxTessEvaluationOutputComponents = 128;
|
||||
Resources.maxTessEvaluationTextureImageUnits = 16;
|
||||
Resources.maxTessEvaluationUniformComponents = 1024;
|
||||
Resources.maxTessPatchComponents = 120;
|
||||
// The tessellation block is shared with glGetIntegerv through Types.h; see the
|
||||
// "Never move one of these without the other" note there.
|
||||
Resources.maxTessControlInputComponents = MAX_TESS_CONTROL_INPUT_COMPONENTS;
|
||||
Resources.maxTessControlOutputComponents = MAX_TESS_CONTROL_OUTPUT_COMPONENTS;
|
||||
Resources.maxTessControlTextureImageUnits = MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS;
|
||||
Resources.maxTessControlUniformComponents = MAX_TESS_CONTROL_UNIFORM_COMPONENTS;
|
||||
Resources.maxTessControlTotalOutputComponents = MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS;
|
||||
Resources.maxTessEvaluationInputComponents = MAX_TESS_EVALUATION_INPUT_COMPONENTS;
|
||||
Resources.maxTessEvaluationOutputComponents = MAX_TESS_EVALUATION_OUTPUT_COMPONENTS;
|
||||
Resources.maxTessEvaluationTextureImageUnits = MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS;
|
||||
Resources.maxTessEvaluationUniformComponents = MAX_TESS_EVALUATION_UNIFORM_COMPONENTS;
|
||||
Resources.maxTessPatchComponents = MAX_TESS_PATCH_COMPONENTS;
|
||||
Resources.maxPatchVertices = 32;
|
||||
Resources.maxTessGenLevel = 64;
|
||||
Resources.maxViewports = 16;
|
||||
@@ -148,9 +172,6 @@ namespace MobileGL {
|
||||
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
|
||||
Resources.maxTransformFeedbackBuffers = 4;
|
||||
Resources.maxTransformFeedbackInterleavedComponents = 64;
|
||||
Resources.maxCullDistances = 8;
|
||||
Resources.maxCombinedClipAndCullDistances = 8;
|
||||
Resources.maxSamples = 4;
|
||||
Resources.maxMeshOutputVerticesNV = 256;
|
||||
Resources.maxMeshOutputPrimitivesNV = 512;
|
||||
Resources.maxMeshWorkGroupSizeX_NV = 32;
|
||||
@@ -180,12 +201,37 @@ namespace MobileGL {
|
||||
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
|
||||
Resources.maxCombinedImageUnitsAndFragmentOutputs =
|
||||
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
|
||||
// GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES and
|
||||
// GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS are the SAME token (0x8F39), so
|
||||
// the two glslang fields have to carry the same value: glGetIntegerv answers this
|
||||
// expression while gl_MaxCombinedShaderOutputResources expanded from a stale
|
||||
// literal 8, and the CTS compares the two directly.
|
||||
Resources.maxCombinedShaderOutputResources =
|
||||
Resources.maxCombinedImageUnitsAndFragmentOutputs;
|
||||
Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms;
|
||||
Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
|
||||
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
|
||||
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
|
||||
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
|
||||
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
|
||||
// The texture-image-unit family and the draw-buffer count. These were stock
|
||||
// glslang defaults (32 / 32 / 80 / 32) that had nothing to do with what
|
||||
// glGetIntegerv answers off the same backend, and the divergence is a live
|
||||
// correctness bug rather than a reporting one: gl_MaxDrawBuffers = 32 makes
|
||||
// glslang ACCEPT a fragment output at location 8..31 that the runtime cannot
|
||||
// bind, and gl_MaxCombinedTextureImageUnits = 80 under-reports a device that
|
||||
// really has 96.
|
||||
Resources.maxTextureImageUnits = dynamicParameters.MaxTextureImageUnits;
|
||||
Resources.maxVertexTextureImageUnits = dynamicParameters.MaxVertexTextureImageUnits;
|
||||
Resources.maxCombinedTextureImageUnits = dynamicParameters.MaxCombinedTextureImageUnits;
|
||||
Resources.maxDrawBuffers = dynamicParameters.MaxDrawBuffers;
|
||||
// The same number glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) reports and the same one
|
||||
// reflection records vertex inputs against - see ResolveMaxVertexAttribs.
|
||||
Resources.maxVertexAttribs = ResolveMaxVertexAttribs(
|
||||
env ? env->HasBackend() : (activeBackend != nullptr), dynamicParameters.MaxVertexAttribs);
|
||||
// gl_MaxSamples, floored exactly as GL_Getter::GetAdvertisedMaxSamples floors
|
||||
// GL_MAX_SAMPLES. It also sizes gl_SampleMask[] / gl_SampleMaskIn[].
|
||||
Resources.maxSamples = std::max(dynamicParameters.MaxSamples, MIN_ADVERTISED_MAX_SAMPLES);
|
||||
// Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for
|
||||
// i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the
|
||||
// same number, so tracking the backend limit is what turns "the program links,
|
||||
@@ -194,6 +240,14 @@ namespace MobileGL {
|
||||
// also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances
|
||||
// agree, which KHR-GLxx.clip_distance.coverage compares directly.
|
||||
Resources.maxClipDistances = dynamicParameters.MaxClipDistances;
|
||||
// The cull pair, for the same reason and with a sharper edge: cull distance
|
||||
// discards the WHOLE primitive, so a shader that gets to declare gl_CullDistance
|
||||
// on a backend that cannot host one does not render subtly wrong pixels, it
|
||||
// renders nothing at all. These were literal 8s that no backend was ever asked
|
||||
// about; a backend without cull distances now reports 0 and glslang rejects the
|
||||
// declaration with a diagnostic the application can read.
|
||||
Resources.maxCullDistances = dynamicParameters.MaxCullDistances;
|
||||
Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances;
|
||||
|
||||
// The compute work-group limits are the env's, not the backend parameters': they
|
||||
// are the only ones that come from a REAL indexed driver query, which
|
||||
@@ -242,7 +296,15 @@ namespace MobileGL {
|
||||
tshader->setStrings(src, 1);
|
||||
tshader->setNanMinMaxClamp(true);
|
||||
tshader->setInvertY(true);
|
||||
tshader->setPreamble("#undef VULKAN\n");
|
||||
// The custom preamble is glslang string -1, which CPPdefine exempts from the
|
||||
// "names beginning with GL_ can't be (un)defined" rule - so it is the only place
|
||||
// an ES source's extension macros can be put back after PreprocessShaderSource
|
||||
// rewrote its #version to desktop and cost it glslang's ES preamble. Empty for
|
||||
// every source that was not rewritten from ES, which is almost all of them.
|
||||
//
|
||||
// setPreamble stores the POINTER, so the buffer has to outlive parse() below.
|
||||
const String preamble = String("#undef VULKAN\n") + CollectEsPreambleMacroDefines(source);
|
||||
tshader->setPreamble(preamble.c_str());
|
||||
if (flags & ShaderCompileBits::CompileForOpenGL) {
|
||||
tshader->setEnvInput(glslang::EShSourceGlsl, lang, glslang::EShClientVulkan, 450);
|
||||
tshader->setEnvClient(glslang::EShClientOpenGL, glslang::EShTargetOpenGL_450);
|
||||
@@ -487,12 +549,28 @@ namespace MobileGL {
|
||||
attrib.explicitFragmentOutIndices,
|
||||
attrib.explicitOpaqueUniformBindings,
|
||||
attrib.storageBlocksWithoutBinding,
|
||||
attrib.uniformBlocksWithoutBinding);
|
||||
attrib.uniformBlocksWithoutBinding,
|
||||
&attrib.resourceBindingLimits,
|
||||
attrib.resourceBindingViolation);
|
||||
break;
|
||||
}
|
||||
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||
|
||||
if (!program->mapIO(resolver.get(), ioMapper.get())) {
|
||||
const bool mapped = program->mapIO(resolver.get(), ioMapper.get());
|
||||
|
||||
// The binding-range verdict is read BEFORE mapIO's own outcome, and unconditionally:
|
||||
// the resolver fills it during the collect phase, which runs whether or not doMap()
|
||||
// later succeeds, and a shader that names an out-of-range binding is rejected for
|
||||
// THAT reason no matter what else the mapper made of it. Reporting the mapper's
|
||||
// generic failure instead would hand the application an info log that says nothing
|
||||
// about the declaration it has to fix.
|
||||
if (attrib.resourceBindingViolation != nullptr && !attrib.resourceBindingViolation->empty()) {
|
||||
ResultInfo r;
|
||||
r.log = *attrib.resourceBindingViolation;
|
||||
r.errc = -5;
|
||||
return std::unexpected(r);
|
||||
}
|
||||
if (!mapped) {
|
||||
ResultInfo r;
|
||||
r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog());
|
||||
r.errc = -4;
|
||||
@@ -1604,6 +1682,197 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The execution model an application-supplied module's entry point must carry for
|
||||
// the shader object it was handed to. glShaderBinary attaches a module to a shader
|
||||
// of a fixed type, and ARB_gl_spirv requires the specialized entry point to match.
|
||||
SpvExecutionModel ExecutionModelForShaderType(GLenum shaderType) {
|
||||
switch (shaderType) {
|
||||
case GL_VERTEX_SHADER:
|
||||
return SpvExecutionModelVertex;
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
return SpvExecutionModelTessellationControl;
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
return SpvExecutionModelTessellationEvaluation;
|
||||
case GL_GEOMETRY_SHADER:
|
||||
return SpvExecutionModelGeometry;
|
||||
case GL_COMPUTE_SHADER:
|
||||
return SpvExecutionModelGLCompute;
|
||||
case GL_FRAGMENT_SHADER:
|
||||
default:
|
||||
return SpvExecutionModelFragment;
|
||||
}
|
||||
}
|
||||
// The decorated capture layout, as the equivalent glTransformFeedbackVaryings
|
||||
// request. GL 4.6 core 11.1.2.1 / ARB_transform_feedback3 give the name list two
|
||||
// pseudo-varyings that are exactly what a decoration layout needs: gl_NextBuffer
|
||||
// moves to the next capture buffer, and gl_SkipComponentsN (N in 1..4) advances the
|
||||
// cursor without capturing. Together they can express any offset/stride layout
|
||||
// whose offsets are component-aligned, which SPIR-V's are (Offset is in bytes and
|
||||
// xfb offsets are four-byte aligned by rule).
|
||||
Vector<String> BuildXfbVaryingRequest(const Vector<SpirvXfbCapture>& captures) {
|
||||
Vector<String> names;
|
||||
if (captures.empty()) return names;
|
||||
|
||||
auto emitSkip = [&names](Uint32 components) {
|
||||
while (components > 0) {
|
||||
const Uint32 step = std::min<Uint32>(components, 4);
|
||||
names.push_back("gl_SkipComponents" + std::to_string(step));
|
||||
components -= step;
|
||||
}
|
||||
};
|
||||
|
||||
Uint32 currentBuffer = captures.front().buffer;
|
||||
Uint32 cursorComponents = 0;
|
||||
Uint32 currentStride = 0;
|
||||
// Buffers below the first captured one still have to be stepped over, so the
|
||||
// Nth gl_NextBuffer really does land on buffer N.
|
||||
for (Uint32 buffer = 0; buffer < currentBuffer; ++buffer) {
|
||||
names.push_back("gl_NextBuffer");
|
||||
}
|
||||
for (const SpirvXfbCapture& capture : captures) {
|
||||
if (capture.buffer != currentBuffer) {
|
||||
// Pad the buffer being left out to its declared stride, so the record
|
||||
// size the module asked for survives.
|
||||
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
|
||||
for (Uint32 buffer = currentBuffer; buffer < capture.buffer; ++buffer) {
|
||||
names.push_back("gl_NextBuffer");
|
||||
}
|
||||
currentBuffer = capture.buffer;
|
||||
cursorComponents = 0;
|
||||
currentStride = 0;
|
||||
}
|
||||
const Uint32 offsetComponents = capture.offset / 4;
|
||||
if (offsetComponents > cursorComponents) emitSkip(offsetComponents - cursorComponents);
|
||||
names.push_back(capture.name);
|
||||
cursorComponents = offsetComponents + capture.componentCount;
|
||||
currentStride = std::max(currentStride, capture.stride);
|
||||
}
|
||||
if (currentStride / 4 > cursorComponents) emitSkip(currentStride / 4 - cursorComponents);
|
||||
return names;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) {
|
||||
ResultInfo r;
|
||||
r.errc = -6;
|
||||
if (spirv.size() < 5) {
|
||||
r.log = "Error: [ARB_gl_spirv] the module is too short to be SPIR-V.";
|
||||
return std::unexpected(r);
|
||||
}
|
||||
// 0x07230203 is SPIR-V's magic number. A module in the other byte order is a
|
||||
// legal SPIR-V file but NOT one glShaderBinary accepts: ARB_gl_spirv fixes the
|
||||
// word order to the host's.
|
||||
if (spirv[0] != 0x07230203u) {
|
||||
r.log = "Error: [ARB_gl_spirv] the module does not begin with the SPIR-V magic number.";
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
PrepareSpirvValidation();
|
||||
spvtools::SpirvTools tools(SPV_ENV_OPENGL_4_5);
|
||||
String diagnostics;
|
||||
tools.SetMessageConsumer([&diagnostics](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
if (!diagnostics.empty()) diagnostics += "\n";
|
||||
diagnostics += message ? message : "";
|
||||
});
|
||||
if (!tools.Validate(spirv.data(), spirv.size())) {
|
||||
r.log = "Error: [ARB_gl_spirv] the module failed SPIR-V validation:\n" + diagnostics;
|
||||
return std::unexpected(r);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<ShaderCompiler::SpecializedModule> ShaderCompiler::SpecializeAndDecompileSpirvModule(
|
||||
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
|
||||
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
|
||||
SpecializationFailure& outFailure) {
|
||||
outFailure = SpecializationFailure::None;
|
||||
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
if (!session.IsTranspileReady()) {
|
||||
// SPIRV-Cross could not parse the module. glShaderBinary's spirv-val pass is a
|
||||
// validity check, not a parseability one, so this is reachable with a module
|
||||
// that validates - hence a diagnosis rather than the null dereference the
|
||||
// unchecked constructor used to walk into.
|
||||
outFailure = SpecializationFailure::ModuleRejected;
|
||||
ResultInfo r;
|
||||
r.errc = -11;
|
||||
r.log = "Error: [ARB_gl_spirv] the module could not be parsed:\n" +
|
||||
String(session.GetLastErrorString());
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
Uint32 unknownConstantId = 0;
|
||||
if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) {
|
||||
outFailure = SpecializationFailure::UnknownConstantId;
|
||||
ResultInfo r;
|
||||
r.errc = -7;
|
||||
r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) +
|
||||
" is not a specialization constant of this module.";
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
// No `if (!entryPoint.empty())` guard any more. ARB_gl_spirv makes pEntryPoint the
|
||||
// name of the entry point to specialize, and no module carries one named ""; the
|
||||
// guard turned an empty name into "whichever entry point happens to be default",
|
||||
// which is neither what the application asked for nor an error it was told about.
|
||||
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
|
||||
SPVC_SUCCESS) {
|
||||
outFailure = SpecializationFailure::UnknownEntryPoint;
|
||||
ResultInfo r;
|
||||
r.errc = -8;
|
||||
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint +
|
||||
"' for this shader stage:\n" + String(session.GetLastErrorString());
|
||||
return std::unexpected(r);
|
||||
}
|
||||
|
||||
// Read the declared capture layout, then REMOVE the decorations that describe it.
|
||||
// Both halves matter: without the read a SPIR-V program captures nothing, and
|
||||
// without the strip the decorations round-trip through the emitted GLSL back into
|
||||
// the regenerated SPIR-V, where DirectGLES's ESSL hop refuses them outright and
|
||||
// loses the stage. See SpvcSession::StripTransformFeedbackDecorations.
|
||||
SpecializedModule specialized;
|
||||
specialized.xfbVaryings = BuildXfbVaryingRequest(session.ReflectTransformFeedbackCaptures());
|
||||
session.StripTransformFeedbackDecorations();
|
||||
|
||||
spvc_compiler_options options;
|
||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) {
|
||||
outFailure = SpecializationFailure::ModuleRejected;
|
||||
ResultInfo r;
|
||||
r.errc = -9;
|
||||
r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module.";
|
||||
return std::unexpected(r);
|
||||
}
|
||||
// DESKTOP 4.60, not the ESSL 3.20 DecompileShader emits: this source goes back in
|
||||
// at the FRONT of the pipeline, to be parsed by glslang exactly like an
|
||||
// application's own GLSL, and every one of MobileGL's source-level passes is
|
||||
// written against the desktop dialect. The ESSL hop happens later and unchanged,
|
||||
// out of the SPIR-V this re-parse produces.
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 460);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
|
||||
// Vulkan semantics OFF is what makes this a GL source: descriptor sets collapse
|
||||
// onto GL binding points, push constants become a uniform block, and - the point
|
||||
// of the specialization pass above - every specialization constant is folded in
|
||||
// as a literal instead of re-emitted as layout(constant_id = N).
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, SPVC_TRUE);
|
||||
session.SetOptions(options);
|
||||
|
||||
const char* emitted = nullptr;
|
||||
session.Compile(&emitted);
|
||||
if (!emitted) {
|
||||
outFailure = SpecializationFailure::ModuleRejected;
|
||||
ResultInfo r;
|
||||
r.errc = -10;
|
||||
r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" +
|
||||
String(session.GetLastErrorString());
|
||||
return std::unexpected(r);
|
||||
}
|
||||
specialized.glsl = String(emitted);
|
||||
return specialized;
|
||||
}
|
||||
|
||||
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
|
||||
spvc_compiler_options options;
|
||||
session.CreateOptions(&options);
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// A GLSL float literal for a tessellation level, for the pass-through tessellation
|
||||
// control stage both backends synthesize when a program has an evaluation stage and no
|
||||
// control stage. Shared so the two generators cannot disagree about what a level means.
|
||||
//
|
||||
// GL 4.6 core 11.2.2 discards a patch only when a relevant OUTER level is <= 0; every
|
||||
// other value is CLAMPED into [1, MAX_TESS_GEN_LEVEL]. So "draw nothing" is reserved
|
||||
// for the values that really mean it, and everything else has to survive the trip
|
||||
// through text: a shortest-round-trip spelling, because a fixed-decimal one flushes
|
||||
// small positive levels to zero, and always with a '.' or an exponent, because a bare
|
||||
// digit sequence is an INT literal and `gl_TessLevelOuter[0] = 1;` does not compile.
|
||||
String TessellationLevelLiteral(Float value);
|
||||
|
||||
class ShaderCompiler {
|
||||
public:
|
||||
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
|
||||
@@ -393,6 +405,78 @@ namespace MobileGL {
|
||||
bool enableSpirvValidation = false);
|
||||
static Result<String> DecompileShader(SpvcSession& session);
|
||||
|
||||
// ---- GL_ARB_gl_spirv ----
|
||||
// Turn an APPLICATION-supplied SPIR-V module into the desktop GLSL the ordinary
|
||||
// compile pipeline consumes.
|
||||
//
|
||||
// Why a round trip rather than handing the module straight to the backends. SPIR-V
|
||||
// is not where MobileGL's pipeline STARTS: a program's whole GL-visible surface -
|
||||
// every glGetActiveUniform, every uniform location, every block index, the
|
||||
// transform-feedback layout, the default-block UBO routing - is reflected out of
|
||||
// glslang's TProgram at link (ProgramLinkTask::SnapshotGlslangReflection), and
|
||||
// glslang can only build one from a GLSL parse. Injecting the module at
|
||||
// ProgramSpirvTask instead would skip the link entirely and leave every one of
|
||||
// those queries answering nothing. Decompiling puts the application's module at
|
||||
// the head of the SAME pipeline, so reflection, the relaxed default-block
|
||||
// lowering, both backends and every memo tier work on it unchanged.
|
||||
//
|
||||
// What it costs, stated plainly: names. A module stripped of OpName (which
|
||||
// ARB_gl_spirv permits, and the conformance suite deliberately does) comes back
|
||||
// with SPIRV-Cross's generated identifiers rather than with none, so the
|
||||
// *_MAX_LENGTH queries answer those instead of 1.
|
||||
//
|
||||
// `entryPoint` selects among several OpEntryPoint of this stage's execution
|
||||
// model; an empty string means "whichever one is there". The specialization
|
||||
// constants glSpecializeShader supplied are applied in the same pass - SPIRV-Cross
|
||||
// folds each into the emitted source as a literal once Vulkan semantics are off,
|
||||
// which is exactly what "specialize, then compile" means for a GLSL consumer.
|
||||
//
|
||||
// `constantIds` and `constantValues` are the parallel arrays the entry point
|
||||
// takes. A constant id the module does not declare is GL_INVALID_VALUE per the
|
||||
// extension; it is reported through the error log rather than silently ignored.
|
||||
// Why the caller needs a REASON and not just a failure: ARB_gl_spirv splits the
|
||||
// ways specialization can fail into two groups with different GL surfaces. A bad
|
||||
// entry-point name and a constant id the module does not declare are enumerated
|
||||
// errors - GL_INVALID_VALUE, and, being errors, they must leave the shader object
|
||||
// exactly as it was. Everything else (a module SPIRV-Cross cannot translate) is a
|
||||
// COMPILE failure, reported through COMPILE_STATUS and the info log like any other
|
||||
// glCompileShader outcome. Returning one undifferentiated error is what made both
|
||||
// groups look like the second.
|
||||
enum class SpecializationFailure {
|
||||
None,
|
||||
UnknownConstantId, // GL_INVALID_VALUE
|
||||
UnknownEntryPoint, // GL_INVALID_VALUE
|
||||
ModuleRejected, // COMPILE_STATUS false + info log
|
||||
};
|
||||
|
||||
// What a specialized module turns into: the GLSL the ordinary pipeline compiles,
|
||||
// plus the transform-feedback capture the module DECLARED, re-expressed as the
|
||||
// glTransformFeedbackVaryings request that produces the same layout.
|
||||
//
|
||||
// The re-expression is the whole design. ARB_gl_spirv makes XfbBuffer/XfbStride/
|
||||
// Offset decorations the only way a SPIR-V program declares capture, and MobileGL's
|
||||
// capture machinery - the frontend packer, DirectGLES's forwarding to the ES
|
||||
// driver, DirectVulkan's XfbCaptureDecoratePass - is driven entirely by a name
|
||||
// list. Translating the decorations into the equivalent name list (with
|
||||
// ARB_transform_feedback3's gl_NextBuffer / gl_SkipComponentsN spelling carrying
|
||||
// the buffer breaks and the gaps) hands a SPIR-V program to the machinery that
|
||||
// already exists, instead of teaching every consumer a second declaration form.
|
||||
struct SpecializedModule {
|
||||
String glsl;
|
||||
Vector<String> xfbVaryings;
|
||||
GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
|
||||
};
|
||||
|
||||
static Result<SpecializedModule> SpecializeAndDecompileSpirvModule(
|
||||
const Vector<Uint32>& spirv, GLenum shaderType, const String& entryPoint,
|
||||
const Vector<Uint32>& constantIds, const Vector<Uint32>& constantValues,
|
||||
SpecializationFailure& outFailure);
|
||||
|
||||
// spirv-val over an application-supplied module, against the environment MobileGL
|
||||
// parses and emits under. glShaderBinary is where a malformed module has to be
|
||||
// caught: past it the words reach SPIRV-Cross, which is not a validator.
|
||||
static Result<void> ValidateSpirvModule(const Vector<Uint32>& spirv);
|
||||
|
||||
// Parses one trivial shader in each configuration the production path can
|
||||
// reach, on the calling thread, so the built-in symbol tables those
|
||||
// configurations need are already cached before any worker asks for one.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <utility>
|
||||
#include <Config.h>
|
||||
@@ -234,15 +235,132 @@ namespace {
|
||||
// Whether the parsed #version directive is a well-formed one MobileGL should rewrite. A
|
||||
// malformed directive (see IsRecognizedGlslVersion) is left alone for glslang to reject.
|
||||
bool hasValidVersionDirective = false;
|
||||
// Every extension the source NAMES in an "#extension <name> : <behavior>" directive, and
|
||||
// the subset whose behavior switches it on. Both are needed and they are not the same
|
||||
// question: glslang's ES preamble defines an extension's macro whatever behavior the
|
||||
// shader later asks for (it is a preamble, it runs first), while whether gl_NumSamples is
|
||||
// a legal identifier depends on the extension actually being ENABLED.
|
||||
std::set<MobileGL::String> namedExtensions;
|
||||
std::set<MobileGL::String> enabledExtensions;
|
||||
// Byte ranges [begin, end) of every #version directive AFTER the first that repeats it
|
||||
// exactly - same version number, same profile, both well-formed. See
|
||||
// BlankRedundantVersionDirectives for why these are tolerated and nothing else is.
|
||||
Vector<std::pair<SizeT, SizeT>> redundantVersionDirectives;
|
||||
|
||||
bool HasVersionDirective() const { return versionDirectiveStart != MobileGL::String::npos; }
|
||||
};
|
||||
|
||||
struct ParsedVersionDirective {
|
||||
unsigned version = 0;
|
||||
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
|
||||
bool isValid = false;
|
||||
};
|
||||
|
||||
// glslang's #extension implication graph, transcribed from
|
||||
// TParseVersions::updateExtensionBehavior (Versions.cpp:1039-1064). Naming one of these
|
||||
// extensions applies the SAME behavior to every name it implies, so a source that says
|
||||
// `#extension GL_ANDROID_extension_pack_es31a : require` has really required all twelve AEP
|
||||
// members - and glslang's ES gl_NumSamples gate reads GL_OES_sample_variables, one of them.
|
||||
//
|
||||
// Transcribed rather than approximated: the AEP membership list is glslang's, and a guess that
|
||||
// drifts from it would make MobileGL accept or reject a shader glslang does not.
|
||||
// GL_KHR_blend_equation_advanced is in the list for completeness even though it has no ES
|
||||
// preamble macro - IsEsOnlyPreambleExtensionMacro filters it out on its own.
|
||||
const Vector<std::pair<const char*, Vector<const char*>>>& GetExtensionImplications() {
|
||||
static const Vector<std::pair<const char*, Vector<const char*>>> kImplications = {
|
||||
{"GL_ANDROID_extension_pack_es31a",
|
||||
{"GL_KHR_blend_equation_advanced", "GL_OES_sample_variables", "GL_OES_shader_image_atomic",
|
||||
"GL_OES_shader_multisample_interpolation", "GL_OES_texture_storage_multisample_2d_array",
|
||||
"GL_EXT_geometry_shader", "GL_EXT_gpu_shader5", "GL_EXT_primitive_bounding_box",
|
||||
"GL_EXT_shader_io_blocks", "GL_EXT_tessellation_shader", "GL_EXT_texture_buffer",
|
||||
"GL_EXT_texture_cube_map_array"}},
|
||||
// geometry / tessellation to io_blocks
|
||||
{"GL_EXT_geometry_shader", {"GL_EXT_shader_io_blocks"}},
|
||||
{"GL_OES_geometry_shader", {"GL_OES_shader_io_blocks"}},
|
||||
{"GL_EXT_tessellation_shader", {"GL_EXT_shader_io_blocks"}},
|
||||
{"GL_OES_tessellation_shader", {"GL_OES_shader_io_blocks"}},
|
||||
};
|
||||
return kImplications;
|
||||
}
|
||||
|
||||
// Closes `extensions` under the graph above. glslang propagates by RE-ENTERING
|
||||
// updateExtensionBehavior, so the propagation is transitive (AEP -> GL_EXT_geometry_shader ->
|
||||
// GL_EXT_shader_io_blocks); the fixed-point loop below is that re-entry.
|
||||
void AddImpliedExtensions(std::set<MobileGL::String>& extensions) {
|
||||
if (extensions.empty()) return;
|
||||
bool grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const auto& [source, implied] : GetExtensionImplications()) {
|
||||
if (extensions.count(source) == 0) continue;
|
||||
for (const char* name : implied) {
|
||||
grew |= extensions.insert(name).second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reads "<digits> [profile]" out of a "#version" directive whose keyword ends at `probe`, and
|
||||
// decides whether it is one MobileGL is willing to rewrite. `code` must be the masked source,
|
||||
// so a trailing comment has already become blanks.
|
||||
bool ParseVersionDirectiveBody(const MobileGL::String& code, SizeT probe, SizeT lineEnd,
|
||||
ParsedVersionDirective& out) {
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
unsigned version = 0;
|
||||
bool hasVersionDigits = false;
|
||||
while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') {
|
||||
hasVersionDigits = true;
|
||||
version = version * 10 + static_cast<unsigned>(code[probe] - '0');
|
||||
probe++;
|
||||
}
|
||||
if (!hasVersionDigits) return false;
|
||||
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
const MobileGL::String profileToken = ReadDirectiveIdentifier(code, probe, lineEnd);
|
||||
bool profileTokenValid = true;
|
||||
MobileGL::ShaderProfile profile = MobileGL::ShaderProfile::Core;
|
||||
if (profileToken.empty() || profileToken == "core") {
|
||||
profile = MobileGL::ShaderProfile::Core;
|
||||
} else if (profileToken == "es" || profileToken == "ES") {
|
||||
profile = MobileGL::ShaderProfile::ES;
|
||||
} else if (profileToken == "compatibility") {
|
||||
profile = MobileGL::ShaderProfile::Compatibility;
|
||||
} else {
|
||||
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any downstream
|
||||
// routing, but mark the directive malformed.
|
||||
profile = MobileGL::ShaderProfile::Core;
|
||||
profileTokenValid = false;
|
||||
}
|
||||
// Comments are already masked to spaces, so anything non-blank left on the line is real
|
||||
// trailing garbage: "#version 330 foobar" / "#version 330.0".
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
const bool hasTrailingTokens = probe < lineEnd;
|
||||
|
||||
out.version = version;
|
||||
out.profile = profile;
|
||||
out.isValid = IsRecognizedGlslVersion(version) && profileTokenValid && !hasTrailingTokens;
|
||||
return true;
|
||||
}
|
||||
|
||||
ShaderLanguageInfo InspectShaderLanguage(const MobileGL::String& source) {
|
||||
const MobileGL::String code = MaskCommentsAndQuotedText(source);
|
||||
ShaderLanguageInfo info;
|
||||
info.hasUtf8Bom = HasUtf8Bom(source);
|
||||
|
||||
// An exact repeat of the accepted directive, wherever on the line it sits. Recorded for
|
||||
// BlankRedundantVersionDirectives; never called before a valid first directive was found,
|
||||
// which is what keeps a LONE misplaced #version rejected.
|
||||
const auto recordIfRedundant = [&info, &code](SizeT hashPos, SizeT lineEnd) {
|
||||
if (!info.hasValidVersionDirective) return;
|
||||
SizeT probe = hashPos + 1;
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
if (ReadDirectiveIdentifier(code, probe, lineEnd) != "version") return;
|
||||
ParsedVersionDirective parsed;
|
||||
if (!ParseVersionDirectiveBody(code, probe, lineEnd, parsed)) return;
|
||||
if (!parsed.isValid || parsed.version != info.version || parsed.profile != info.profile) return;
|
||||
info.redundantVersionDirectives.push_back({hashPos, lineEnd});
|
||||
};
|
||||
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart < code.size()) {
|
||||
SizeT lineEnd = code.find('\n', lineStart);
|
||||
@@ -256,46 +374,49 @@ namespace {
|
||||
probe = 3;
|
||||
}
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
if (probe < lineEnd && code[probe] == '#') {
|
||||
if (probe >= lineEnd || code[probe] != '#') {
|
||||
// A directive that is not first on its line is not a directive at all - except for
|
||||
// the one case glShaderSource creates on its own: two strings each headed by a
|
||||
// #version splice the second into the tail of the first. Only an EXACT repeat of
|
||||
// the directive already accepted is recognized here; see
|
||||
// BlankRedundantVersionDirectives for why that one is tolerated and nothing else.
|
||||
//
|
||||
// Gated on a directive having been accepted already, so an ordinary shader - which
|
||||
// has none of these - pays nothing at all before its #version line.
|
||||
//
|
||||
// BOUNDED TO THE LINE, and that is not a detail. std::string::find(char, pos) has
|
||||
// no end bound, so a `code.find('#', probe)` filtered afterwards by
|
||||
// `hashPos < lineEnd` scans from this line to the END OF THE SOURCE whenever no
|
||||
// '#' follows - which is the ordinary shape of a resolved shader-pack source (one
|
||||
// leading #version, nothing after it), and it makes this whole sweep quadratic in
|
||||
// shader size. A 131 KB glsl-transformer output in .trace-work has exactly one '#'
|
||||
// in the file. Searching the line span is behaviour-identical: every hashPos the
|
||||
// unbounded form could accept already had to satisfy hashPos < lineEnd.
|
||||
if (info.hasValidVersionDirective && probe < lineEnd) {
|
||||
const void* hash = std::memchr(code.data() + probe, '#', lineEnd - probe);
|
||||
if (hash != nullptr) {
|
||||
recordIfRedundant(static_cast<SizeT>(static_cast<const char*>(hash) - code.data()),
|
||||
lineEnd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const SizeT directiveStart = probe;
|
||||
probe++;
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
const MobileGL::String directive = ReadDirectiveIdentifier(code, probe, lineEnd);
|
||||
|
||||
if (directive == "version" && !info.HasVersionDirective()) {
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
unsigned version = 0;
|
||||
bool hasVersionDigits = false;
|
||||
while (probe < lineEnd && code[probe] >= '0' && code[probe] <= '9') {
|
||||
hasVersionDigits = true;
|
||||
version = version * 10 + static_cast<unsigned>(code[probe] - '0');
|
||||
probe++;
|
||||
}
|
||||
if (hasVersionDigits) {
|
||||
info.version = version;
|
||||
if (directive == "version") {
|
||||
ParsedVersionDirective parsed;
|
||||
if (ParseVersionDirectiveBody(code, probe, lineEnd, parsed)) {
|
||||
if (!info.HasVersionDirective()) {
|
||||
info.version = parsed.version;
|
||||
info.profile = parsed.profile;
|
||||
info.versionDirectiveStart = directiveStart;
|
||||
info.versionDirectiveEnd = lineEnd + (hasLineBreak ? 1 : 0);
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
const MobileGL::String profile = ReadDirectiveIdentifier(code, probe, lineEnd);
|
||||
bool profileTokenValid = true;
|
||||
if (profile.empty() || profile == "core") {
|
||||
info.profile = MobileGL::ShaderProfile::Core;
|
||||
} else if (profile == "es" || profile == "ES") {
|
||||
info.profile = MobileGL::ShaderProfile::ES;
|
||||
} else if (profile == "compatibility") {
|
||||
info.profile = MobileGL::ShaderProfile::Compatibility;
|
||||
info.hasValidVersionDirective = parsed.isValid;
|
||||
} else {
|
||||
// "#version 330 foo": an unrecognized profile keyword. Keep Core for any
|
||||
// downstream routing, but mark the directive malformed.
|
||||
info.profile = MobileGL::ShaderProfile::Core;
|
||||
profileTokenValid = false;
|
||||
recordIfRedundant(directiveStart, lineEnd);
|
||||
}
|
||||
// Comments are already masked to spaces, so anything non-blank left on the
|
||||
// line is real trailing garbage: "#version 330 foobar" / "#version 330.0".
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
const bool hasTrailingTokens = probe < lineEnd;
|
||||
info.hasValidVersionDirective =
|
||||
IsRecognizedGlslVersion(info.version) && profileTokenValid && !hasTrailingTokens;
|
||||
}
|
||||
} else if (directive == "extension") {
|
||||
SkipDirectiveWhitespace(code, probe, lineEnd);
|
||||
@@ -309,6 +430,10 @@ namespace {
|
||||
extension == "GL_NV_gpu_shader5";
|
||||
const bool enablesExtension = behavior == "enable" || behavior == "require" ||
|
||||
behavior == "warn";
|
||||
if (!extension.empty()) {
|
||||
info.namedExtensions.insert(extension);
|
||||
if (enablesExtension) info.enabledExtensions.insert(extension);
|
||||
}
|
||||
// Gate the whole source if it ever opts into either extension. This is deliberately
|
||||
// conservative around conditional directives and keeps legal sample qualifiers intact.
|
||||
info.enablesGpuShader5 = info.enablesGpuShader5 || (isGpuShader5 && enablesExtension);
|
||||
@@ -319,6 +444,19 @@ namespace {
|
||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||
}
|
||||
|
||||
// Both extension sets are closed under glslang's implication graph BEFORE anyone reads
|
||||
// them, so every consumer sees the same expansion and none of them can forget it. Applied
|
||||
// here rather than at the directive because an implication may be named before its source
|
||||
// (`#extension GL_EXT_shader_io_blocks : disable` then `... AEP : require`), and the
|
||||
// fixed point of the whole set is what glslang's re-entrant propagation ends up at.
|
||||
//
|
||||
// enablesGpuShader5 is deliberately NOT recomputed from the expanded set: it gates the
|
||||
// 460 version escalation on the DESKTOP ARB/NV spellings, and AEP implies the ESSL
|
||||
// GL_EXT_gpu_shader5, a different extension. An ES source is rewritten to 460 core
|
||||
// anyway, so there is nothing for the escalation to do there.
|
||||
AddImpliedExtensions(info.namedExtensions);
|
||||
AddImpliedExtensions(info.enabledExtensions);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -367,9 +505,40 @@ namespace {
|
||||
// (FindAfterVersionDirective -> InspectShaderLanguage). Each branch below leaves the bytes
|
||||
// ahead of the directive untouched apart from the BOM erase, and each replacement text is
|
||||
// exactly one newline-terminated line, so the arithmetic is exact in all three cases.
|
||||
// An exact repeat of the #version directive the shader already declared, blanked out.
|
||||
//
|
||||
// Strictly a repeat: InspectShaderLanguage only records a range here when the FIRST directive
|
||||
// was well-formed and the later one is well-formed, names the same version number and the same
|
||||
// profile, and is therefore semantically a no-op. Everything else - a differing version, a
|
||||
// malformed one, or a lone #version that is simply not first - is left exactly where the
|
||||
// application put it, so KHR-GL33.shaders.preprocessor.directive.version_not_first_statement_*
|
||||
// and the version_invalid_token_* family keep failing to compile the way they must.
|
||||
//
|
||||
// Why tolerate even the repeat: glShaderSource concatenates its strings with nothing added
|
||||
// between them (GL 4.6 core 7.1), and a caller that puts a #version at the head of BOTH strings
|
||||
// gets the second one spliced into the tail of the first - which is exactly what VK-GL-CTS's
|
||||
// ShaderImageLoadStoreBase::BuildProgram does (kGLSLPrec ends without a newline, and
|
||||
// NegativeUniform's own sources begin with "#version 310 es"). Desktop drivers accept it; the
|
||||
// duplicate says nothing new, so honouring it costs no semantics.
|
||||
//
|
||||
// Blanked rather than erased so that every offset in `info` - which was measured against this
|
||||
// same source - stays valid, and so the line count, and with it __LINE__ and every glslang
|
||||
// diagnostic, is untouched.
|
||||
void BlankRedundantVersionDirectives(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
||||
for (const auto& [begin, end] : info.redundantVersionDirectives) {
|
||||
if (begin >= source.size() || end > source.size() || begin >= end) continue;
|
||||
std::fill(source.begin() + static_cast<std::ptrdiff_t>(begin),
|
||||
source.begin() + static_cast<std::ptrdiff_t>(end), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
SizeT NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
|
||||
const SizeT bomBytes = info.hasUtf8Bom ? 3 : 0;
|
||||
|
||||
// First, while every offset in `info` still refers to the untouched source. Each range
|
||||
// lies strictly after the first directive, so nothing below has to account for it.
|
||||
BlankRedundantVersionDirectives(source, info);
|
||||
|
||||
// A malformed #version (329, 331, bad profile, float/trailing tokens) is left exactly as the
|
||||
// application wrote it so glslang rejects it - rewriting it to "#version 330 core" would
|
||||
// silently legalize the CTS directive.version_* rejection cases. Still drop a leading BOM so
|
||||
@@ -1435,6 +1604,215 @@ namespace {
|
||||
"#define gl_DepthRange mg_DepthRange\n";
|
||||
source.insert(afterVersion.Get(source), shim);
|
||||
}
|
||||
|
||||
// Whole-identifier search over an already-masked source. A bare find() would fire on
|
||||
// "mg_NumSamplesFoo" and on the word inside a comment; this fires only on the token.
|
||||
bool MaskedSourceHasIdentifier(const MobileGL::String& masked, MobileGL::StringView identifier) {
|
||||
SizeT pos = 0;
|
||||
while ((pos = masked.find(identifier.data(), pos, identifier.size())) != MobileGL::String::npos) {
|
||||
const SizeT end = pos + identifier.size();
|
||||
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(masked[pos - 1]);
|
||||
const bool hasRightBoundary = end >= masked.size() || !IsIdentifierChar(masked[end]);
|
||||
if (hasLeftBoundary && hasRightBoundary) return true;
|
||||
pos = end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The extension macros glslang's ES preamble defines and its DESKTOP preamble does not
|
||||
// (TParseVersions::getPreamble, Versions.cpp). Transcribed rather than derived because the
|
||||
// preamble is a string literal inside glslang with no programmatic accessor; the SET is what
|
||||
// matters, and it is stable - these are the AEP/OES/EXT names ESSL has carried since 3.10.
|
||||
//
|
||||
// GL_ES and GL_FRAGMENT_PRECISION_HIGH are DELIBERATELY absent. The shader really is being
|
||||
// compiled as desktop by the time this runs, so flipping an `#ifdef GL_ES` branch would hand
|
||||
// glslang the ESSL half of a shader written to be portable - which is the branch that does not
|
||||
// parse under core 4.60. (GL_FRAGMENT_PRECISION_HIGH is in glslang's desktop preamble anyway.)
|
||||
bool IsEsOnlyPreambleExtensionMacro(const MobileGL::String& name, unsigned version) {
|
||||
// Guarded by an ES version in glslang's preamble; the rest are unconditional.
|
||||
if (name == "GL_NV_shader_noperspective_interpolation") return version >= 300;
|
||||
|
||||
static const std::set<MobileGL::String> kEsOnlyPreambleMacros = {
|
||||
"GL_ANDROID_extension_pack_es31a",
|
||||
"GL_EXT_YUV_target",
|
||||
"GL_EXT_blend_func_extended",
|
||||
"GL_EXT_frag_depth",
|
||||
"GL_EXT_geometry_point_size",
|
||||
"GL_EXT_geometry_shader",
|
||||
"GL_EXT_gpu_shader5",
|
||||
"GL_EXT_primitive_bounding_box",
|
||||
"GL_EXT_shader_implicit_conversions",
|
||||
"GL_EXT_shader_io_blocks",
|
||||
"GL_EXT_shader_texture_lod",
|
||||
"GL_EXT_shadow_samplers",
|
||||
"GL_EXT_tessellation_point_size",
|
||||
"GL_EXT_tessellation_shader",
|
||||
"GL_EXT_texture_buffer",
|
||||
"GL_EXT_texture_cube_map_array",
|
||||
"GL_OES_EGL_image_external",
|
||||
"GL_OES_EGL_image_external_essl3",
|
||||
"GL_OES_geometry_point_size",
|
||||
"GL_OES_geometry_shader",
|
||||
"GL_OES_gpu_shader5",
|
||||
"GL_OES_primitive_bounding_box",
|
||||
"GL_OES_sample_variables",
|
||||
"GL_OES_shader_image_atomic",
|
||||
"GL_OES_shader_io_blocks",
|
||||
"GL_OES_shader_multisample_interpolation",
|
||||
"GL_OES_standard_derivatives",
|
||||
"GL_OES_tessellation_point_size",
|
||||
"GL_OES_tessellation_shader",
|
||||
"GL_OES_texture_3D",
|
||||
"GL_OES_texture_buffer",
|
||||
"GL_OES_texture_cube_map_array",
|
||||
"GL_OES_texture_storage_multisample_2d_array",
|
||||
};
|
||||
return kEsOnlyPreambleMacros.count(name) != 0;
|
||||
}
|
||||
|
||||
// Marker recording that PreprocessShaderSource rewrote an ES-profile source to desktop AND
|
||||
// that the source names at least one extension whose macro glslang's ES preamble would have
|
||||
// defined. The declared ESSL version rides along because two of those macros are themselves
|
||||
// version-gated in glslang.
|
||||
//
|
||||
// A marker rather than a "#define" block, because the macros CANNOT live in the shader text:
|
||||
// glslang rejects "#define GL_..." outright (TParseContext::reservedPpErrorCheck, "names
|
||||
// beginning with GL_ can't be (un)defined") for every string the application supplied - but
|
||||
// deliberately NOT for the preamble strings, which is where its own ES preamble defines them
|
||||
// (CPPdefine's `if (ppToken->loc.string >= 0)` gate; the two preambles sit at string index -2
|
||||
// and -1). So the macros have to reach glslang through TShader::setPreamble, and this marker is
|
||||
// how the decision - which needs the ORIGINAL profile and version, both gone by then - travels
|
||||
// to the compiler. It rides inside the preprocessed source, so the preprocess cache and the
|
||||
// translation cache both key on it for free.
|
||||
constexpr const char* kEsPreambleMarkerPrefix = "/*mobilegl-es-preamble:";
|
||||
|
||||
// The set of macros named by an ES source that the desktop preamble will not define. Shared by
|
||||
// the injector below and by CollectEsPreambleMacroDefines, which re-derives it at compile time
|
||||
// from the marker - one whitelist, one version rule, no chance of the two disagreeing.
|
||||
MobileGL::String BuildEsPreambleMacroList(const MobileGL::String& source, unsigned esVersion) {
|
||||
MobileGL::String macros;
|
||||
// std::set iteration order, so the result is deterministic for the caches and for the
|
||||
// byte-exact preprocessor tests.
|
||||
for (const MobileGL::String& extension : InspectShaderLanguage(source).namedExtensions) {
|
||||
if (!IsEsOnlyPreambleExtensionMacro(extension, esVersion)) continue;
|
||||
macros += "#define " + extension + " 1\n";
|
||||
}
|
||||
return macros;
|
||||
}
|
||||
|
||||
// GetNormalizedVersionDirective rewrites every ES-profile shader to "#version 460 core", so
|
||||
// glslang deduces a desktop profile and emits its DESKTOP preamble - and every ES-only
|
||||
// extension macro the shader is entitled to disappears with it. A CTS shader guarded by
|
||||
// `#if !GL_OES_sample_variables / this is broken / #endif` then takes the broken branch.
|
||||
//
|
||||
// The extension BEHAVIOUR survives the rewrite (glslang honours "#extension X : require" under
|
||||
// either profile), so this is a preamble-fidelity gap and nothing more; restoring the macros is
|
||||
// the whole fix.
|
||||
//
|
||||
// Strictly limited to extensions the source itself NAMES in an #extension directive. Any macro
|
||||
// injected into a desktop parse can flip a preprocessor branch, and the ES preamble carries
|
||||
// three dozen of them - defining the lot would rewrite shaders that never asked.
|
||||
void MarkEsPreambleExtensionMacros(const ShaderLanguageInfo& info, MobileGL::String& source,
|
||||
AfterVersionAnchor& afterVersion) {
|
||||
// Only where the rewrite actually happened: a malformed directive is left for glslang to
|
||||
// reject, and a desktop source already gets the preamble it is entitled to.
|
||||
if (info.profile != MobileGL::ShaderProfile::ES) return;
|
||||
if (!info.hasValidVersionDirective) return;
|
||||
if (info.namedExtensions.empty()) return;
|
||||
|
||||
// namedExtensions is already closed under glslang's implication graph, so a source that
|
||||
// names only GL_ANDROID_extension_pack_es31a marks its twelve members too - glslang's ES
|
||||
// preamble defines all of them, and the CTS-shaped "#if !GL_OES_sample_variables" guard
|
||||
// reads one of them.
|
||||
//
|
||||
// `#extension all : warn` is deliberately NOT honoured here, unlike in the built-in gate.
|
||||
// The two answer different questions: the gate asks "would glslang have this extension
|
||||
// turned on", where `all` genuinely says yes, while this asks "which preamble macros did
|
||||
// the ES -> desktop rewrite take away". glslang's preamble runs BEFORE any #extension line
|
||||
// and defines the ES macros regardless of behavior, so `all` adds no information - and
|
||||
// emitting all thirty-five for a source that named nothing is exactly the broad rewrite
|
||||
// the named-extensions-only policy exists to avoid.
|
||||
const bool hasMacroToRestore =
|
||||
std::any_of(info.namedExtensions.begin(), info.namedExtensions.end(),
|
||||
[&info](const MobileGL::String& extension) {
|
||||
return IsEsOnlyPreambleExtensionMacro(extension, info.version);
|
||||
});
|
||||
// Nothing the desktop preamble is missing: leave the source byte-identical.
|
||||
if (!hasMacroToRestore) return;
|
||||
|
||||
source.insert(afterVersion.Get(source),
|
||||
MobileGL::String(kEsPreambleMarkerPrefix) + std::to_string(info.version) + "*/\n");
|
||||
}
|
||||
|
||||
// "Would glslang have this extension turned on?", mirroring TParseVersions::extensionTurnedOn.
|
||||
//
|
||||
// Two spellings besides the name itself reach it. The implication graph is already folded into
|
||||
// enabledExtensions (AddImpliedExtensions), so only `#extension all : <behavior>` is left:
|
||||
// glslang applies that behavior to EVERY registered extension at once, and rejects `all` with
|
||||
// require/enable outright (Versions.cpp:1136-1141) - so the only spellings that survive are
|
||||
// `all : warn`, which turns everything ON (behavior != EBhDisable), and `all : disable`.
|
||||
// InspectShaderLanguage only records a name in enabledExtensions for enable/require/warn, so
|
||||
// the literal "all" appearing here means `all : warn` and nothing else.
|
||||
bool ExtensionTurnedOn(const ShaderLanguageInfo& info, const char* extension) {
|
||||
return info.enabledExtensions.count(extension) != 0 || info.enabledExtensions.count("all") != 0;
|
||||
}
|
||||
|
||||
// gl_NumSamples is legal in this source only where glslang would have declared it with a
|
||||
// non-SPIR-V target (Initialize.cpp): desktop from 4.00 core, or from 1.30 with
|
||||
// ARB_sample_shading; ESSL from 3.20, or from 3.10 with OES_sample_variables - the last of
|
||||
// which GL_ANDROID_extension_pack_es31a also turns on, via the implication graph.
|
||||
//
|
||||
// The gate matters because the shim ends in "#define gl_NumSamples mg_NumSamples", and a
|
||||
// #define is not scoped by anything: defining it for a source where the built-in does not
|
||||
// exist would silently legalize a shader a conformant implementation rejects.
|
||||
bool SourceMayUseSampleVariables(const ShaderLanguageInfo& info) {
|
||||
if (!info.HasVersionDirective() || !info.hasValidVersionDirective) return false;
|
||||
if (info.profile == MobileGL::ShaderProfile::ES) {
|
||||
if (info.version >= 320) return true;
|
||||
return info.version >= 310 && ExtensionTurnedOn(info, "GL_OES_sample_variables");
|
||||
}
|
||||
if (info.version >= 400) return true;
|
||||
return info.version >= 130 && ExtensionTurnedOn(info, "GL_ARB_sample_shading");
|
||||
}
|
||||
|
||||
// gl_NumSamples has no SPIR-V built-in to lower to, so glslang declares it only when it is NOT
|
||||
// targeting SPIR-V - both the desktop branch and the ES branch of Initialize.cpp wrap the
|
||||
// `uniform int gl_NumSamples;` line in `if (spvVersion.spv == 0)`. MobileGL always targets
|
||||
// SPIR-V (ShaderCompiler sets EShTargetSpv on the OpenGL path as well as the Vulkan one), so
|
||||
// the symbol is never in the table and every shader that reads it dies at compile time with
|
||||
// "'gl_NumSamples' : undeclared identifier".
|
||||
//
|
||||
// Lower it to a real uniform instead. `uniform int mg_NumSamples;` is a default-block uniform,
|
||||
// which the relaxed parse folds into MGL_GLOBAL_UBO - the one buffer BOTH backends already
|
||||
// upload per draw - and the draw path writes the current draw framebuffer's sample count into
|
||||
// it. Deliberately not a link-time constant: one program may be drawn into framebuffers of
|
||||
// different sample counts, and baking the count at link would quietly hand it the wrong one.
|
||||
//
|
||||
// The alternative - deleting the `spvVersion.spv == 0` guard in the glslang fork - is worse,
|
||||
// and not only because it is a fork change: glslang would then place a `gl_`-prefixed member
|
||||
// inside MGL_GLOBAL_UBO, and ESSL reserves `gl_`, so the ES driver would reject SPIRV-Cross's
|
||||
// output on the DirectGLES path.
|
||||
void InjectNumSamplesBuiltinShim(MobileGL::ShaderStage stage, const ShaderLanguageInfo& info,
|
||||
MobileGL::String& source, AfterVersionAnchor& afterVersion) {
|
||||
// gl_NumSamples exists in the fragment stage only, in every profile.
|
||||
if (stage != MobileGL::ShaderStage::Fragment) return;
|
||||
if (!SourceMayUseSampleVariables(info)) return;
|
||||
// Cheap reject before paying for the mask; the token cannot be there if the bytes are not.
|
||||
if (source.find("gl_NumSamples") == MobileGL::String::npos) return;
|
||||
|
||||
const MobileGL::String masked = MaskCommentsAndQuotedText(source);
|
||||
if (!MaskedSourceHasIdentifier(masked, "gl_NumSamples")) return;
|
||||
// Someone already occupies the name - a re-preprocess of an already-shimmed source, or an
|
||||
// application that happens to use it. Either way a second declaration would not compile.
|
||||
if (MaskedSourceHasIdentifier(masked, MobileGL::MG_Util::ShaderTranspiler::NUM_SAMPLES_UNIFORM_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr const char* shim =
|
||||
"uniform int mg_NumSamples;\n"
|
||||
"#define gl_NumSamples mg_NumSamples\n";
|
||||
source.insert(afterVersion.Get(source), shim);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace MobileGL {
|
||||
@@ -1463,6 +1841,13 @@ namespace MobileGL {
|
||||
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
|
||||
NormalizeLineDirectives(source, afterVersion.Get(source));
|
||||
|
||||
// An ES source rewritten to desktop has lost glslang's ES preamble, and the macros
|
||||
// it carried are what the shader's own #if guards read. Keyed off originalLanguage
|
||||
// because the directive has already been rewritten by now and no longer says "es";
|
||||
// the macros themselves are restored through the compiler's preamble, which is why
|
||||
// this only leaves a marker behind (see kEsPreambleMarkerPrefix).
|
||||
MarkEsPreambleExtensionMacros(originalLanguage, source, afterVersion);
|
||||
|
||||
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
|
||||
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
|
||||
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
|
||||
@@ -1487,9 +1872,31 @@ namespace MobileGL {
|
||||
|
||||
ModernizeLegacyGLSL(stage, source, afterVersion);
|
||||
InjectDepthRangeBuiltinShim(stage, source, afterVersion);
|
||||
InjectNumSamplesBuiltinShim(stage, originalLanguage, source, afterVersion);
|
||||
|
||||
}
|
||||
|
||||
String CollectEsPreambleMacroDefines(const String& preprocessedSource) {
|
||||
const SizeT markerStart = preprocessedSource.find(kEsPreambleMarkerPrefix);
|
||||
if (markerStart == String::npos) return {};
|
||||
|
||||
SizeT probe = markerStart + std::char_traits<char>::length(kEsPreambleMarkerPrefix);
|
||||
unsigned esVersion = 0;
|
||||
bool hasDigits = false;
|
||||
while (probe < preprocessedSource.size() && preprocessedSource[probe] >= '0' &&
|
||||
preprocessedSource[probe] <= '9') {
|
||||
hasDigits = true;
|
||||
esVersion = esVersion * 10 + static_cast<unsigned>(preprocessedSource[probe] - '0');
|
||||
if (esVersion > 1000) return {}; // absurd; not a marker this pipeline wrote
|
||||
probe++;
|
||||
}
|
||||
// Only MobileGL's own marker, spelled exactly: a shader that happens to contain the
|
||||
// prefix inside a comment of its own must not be able to steer the preamble.
|
||||
if (!hasDigits || preprocessedSource.compare(probe, 2, "*/") != 0) return {};
|
||||
|
||||
return BuildEsPreambleMacroList(preprocessedSource, esVersion);
|
||||
}
|
||||
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source) {
|
||||
// Re-inspect rather than searching for the literal directive: it is not necessarily at
|
||||
// offset 0 (a BOM or comments may precede it) and a commented-out "#version" elsewhere
|
||||
|
||||
@@ -40,6 +40,27 @@ namespace MobileGL {
|
||||
// accept - can be retried instead of failing to compile.
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
||||
|
||||
// The "#define <EXT> 1" lines an ES-profile source needs restored after
|
||||
// PreprocessShaderSource rewrote its #version to desktop, or "" for every other source.
|
||||
//
|
||||
// glslang defines the OES/AEP extension macros only in its ES preamble
|
||||
// (TParseVersions::getPreamble), selected by the profile it deduces from the directive -
|
||||
// so the rewrite silently takes them away and the shader's own
|
||||
// "#if !GL_OES_sample_variables" guard flips. They cannot simply be written into the
|
||||
// shader text: "#define GL_..." is a hard error for every application-supplied string
|
||||
// (TParseContext::reservedPpErrorCheck). They therefore go into glslang's CUSTOM
|
||||
// PREAMBLE, which sits at string index -1 and is exempt from that check by the same
|
||||
// gate that exempts glslang's own preamble - hence a separate function called by the
|
||||
// compiler rather than another injection pass.
|
||||
//
|
||||
// Deliberately narrow: only extensions the source itself NAMES in an #extension
|
||||
// directive, and never GL_ES or GL_FRAGMENT_PRECISION_HIGH. The shader really is being
|
||||
// compiled as desktop now, and flipping "#ifdef GL_ES" branches would break far more
|
||||
// than it fixes - which is why this is a partial fix by construction. The clean
|
||||
// long-term fix is to stop rewriting ES sources to desktop at all; the comment in
|
||||
// GetNormalizedVersionDirective records why that has not happened.
|
||||
String CollectEsPreambleMacroDefines(const String& preprocessedSource);
|
||||
|
||||
// GLSL reserves a few names glslang happily accepts as identifiers ("packed",
|
||||
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the
|
||||
// compile-error text for the first violation, or nullopt for a clean source.
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "SpvcSession.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
@@ -184,11 +186,29 @@ namespace MobileGL {
|
||||
const SpvId* p_spirv = spirv.data();
|
||||
size_t word_count = spirv.size();
|
||||
|
||||
spvc_context_create(&context);
|
||||
spvc_context_parse_spirv(context, p_spirv, word_count, &ir);
|
||||
spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir, SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
|
||||
&compiler);
|
||||
spvc_compiler_create_shader_resources(compiler, &resources);
|
||||
// Every step is checked, and each guards the next: the C API writes its
|
||||
// out-param only on success, so passing a failed step's null handle to the
|
||||
// step after it is a raw dereference (spvc_context_create_compiler does
|
||||
// `parsed_ir->parsed`, spvc_compiler_create_shader_resources does
|
||||
// `compiler->context`). IsTranspileReady() is how a caller asks whether this
|
||||
// sequence got all the way through.
|
||||
if (spvc_context_create(&context) != SPVC_SUCCESS) {
|
||||
context = nullptr;
|
||||
return;
|
||||
}
|
||||
if (spvc_context_parse_spirv(context, p_spirv, word_count, &ir) != SPVC_SUCCESS) {
|
||||
ir = nullptr;
|
||||
return;
|
||||
}
|
||||
if (spvc_context_create_compiler(context, SPVC_BACKEND_GLSL, ir,
|
||||
SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler) != SPVC_SUCCESS) {
|
||||
compiler = nullptr;
|
||||
return;
|
||||
}
|
||||
if (spvc_compiler_create_shader_resources(compiler, &resources) != SPVC_SUCCESS) {
|
||||
resources = nullptr;
|
||||
return;
|
||||
}
|
||||
} else if (usage & SessionUsageBit::Reflection) {
|
||||
SpvReflectResult result = spvReflectCreateShaderModule(
|
||||
spirv.size() * sizeof(uint32_t), spirv.data(), &reflectModule);
|
||||
@@ -496,6 +516,271 @@ namespace MobileGL {
|
||||
SPVC_CHK_RETURN
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How many 32-bit components a captured variable occupies, which is what the
|
||||
// gl_SkipComponentsN padding below is counted in. Matrices and arrays multiply.
|
||||
Uint32 XfbComponentCount(spvc_compiler compiler, spvc_type_id typeId) {
|
||||
const spvc_type type = spvc_compiler_get_type_handle(compiler, typeId);
|
||||
if (type == nullptr) return 0;
|
||||
Uint32 components = spvc_type_get_vector_size(type) * spvc_type_get_columns(type);
|
||||
const unsigned dimensions = spvc_type_get_num_array_dimensions(type);
|
||||
for (unsigned d = 0; d < dimensions; ++d) {
|
||||
const unsigned length = spvc_type_get_array_dimension(type, d);
|
||||
if (length != 0) components *= length;
|
||||
}
|
||||
// A double occupies two component slots per scalar (GL 4.6 core 11.1.2.1).
|
||||
const spvc_basetype base = spvc_type_get_basetype(type);
|
||||
if (base == SPVC_BASETYPE_FP64 || base == SPVC_BASETYPE_INT64 ||
|
||||
base == SPVC_BASETYPE_UINT64) {
|
||||
components *= 2;
|
||||
}
|
||||
return components;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
// The four gl_PerVertex members, by their GL interface names. These are the only
|
||||
// built-ins GL lets transform feedback capture, and a SPIR-V module names them by
|
||||
// BuiltIn decoration rather than by string - so the mapping has to live somewhere.
|
||||
const char* XfbBuiltInName(SpvBuiltIn builtin) {
|
||||
switch (builtin) {
|
||||
case SpvBuiltInPosition:
|
||||
return "gl_Position";
|
||||
case SpvBuiltInPointSize:
|
||||
return "gl_PointSize";
|
||||
case SpvBuiltInClipDistance:
|
||||
return "gl_ClipDistance";
|
||||
case SpvBuiltInCullDistance:
|
||||
return "gl_CullDistance";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Vector<SpirvXfbCapture> SpvcSession::ReflectTransformFeedbackCaptures() const {
|
||||
Vector<SpirvXfbCapture> captures;
|
||||
if (compiler == nullptr || resources == nullptr) return captures;
|
||||
|
||||
// XfbBuffer/XfbStride sit on the declaring VARIABLE; Offset sits on the variable
|
||||
// for a plain output and on each MEMBER for a block.
|
||||
auto readVariableDecorations = [this](SpvId id, Uint32& outBuffer, Uint32& outStride) {
|
||||
outBuffer = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbBuffer) == SPVC_TRUE
|
||||
? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbBuffer)
|
||||
: 0u;
|
||||
outStride = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbStride) == SPVC_TRUE
|
||||
? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbStride)
|
||||
: 0u;
|
||||
};
|
||||
|
||||
// ---- application outputs: plain variables and application blocks ----
|
||||
const spvc_reflected_resource* outputs = nullptr;
|
||||
SizeT outputCount = 0;
|
||||
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
|
||||
&outputCount) == SPVC_SUCCESS) {
|
||||
for (SizeT i = 0; i < outputCount; ++i) {
|
||||
const spvc_reflected_resource& output = outputs[i];
|
||||
Uint32 buffer = 0;
|
||||
Uint32 stride = 0;
|
||||
readVariableDecorations(output.id, buffer, stride);
|
||||
|
||||
const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id);
|
||||
const unsigned memberCount =
|
||||
type != nullptr && spvc_type_get_basetype(type) == SPVC_BASETYPE_STRUCT
|
||||
? spvc_type_get_num_member_types(type)
|
||||
: 0u;
|
||||
|
||||
if (memberCount == 0) {
|
||||
if (spvc_compiler_has_decoration(compiler, output.id, SpvDecorationOffset) != SPVC_TRUE) {
|
||||
continue;
|
||||
}
|
||||
SpirvXfbCapture capture;
|
||||
capture.name = output.name ? output.name : "";
|
||||
capture.buffer = buffer;
|
||||
capture.stride = stride;
|
||||
capture.offset = spvc_compiler_get_decoration(compiler, output.id, SpvDecorationOffset);
|
||||
capture.componentCount = XfbComponentCount(compiler, output.type_id);
|
||||
if (!capture.name.empty()) captures.push_back(Move(capture));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (unsigned member = 0; member < memberCount; ++member) {
|
||||
if (spvc_compiler_has_member_decoration(compiler, output.base_type_id, member,
|
||||
SpvDecorationOffset) != SPVC_TRUE) {
|
||||
continue;
|
||||
}
|
||||
const char* memberName =
|
||||
spvc_compiler_get_member_name(compiler, output.base_type_id, member);
|
||||
if (memberName == nullptr || *memberName == '\0') continue;
|
||||
SpirvXfbCapture capture;
|
||||
const String blockName = output.name ? String(output.name) : String{};
|
||||
// GL's capture interface spells an application block's member
|
||||
// "Block.member"; a redeclared built-in block contributes its members
|
||||
// by their own names, which the built-in walk below handles.
|
||||
capture.name = blockName.empty() ? String(memberName)
|
||||
: blockName + "." + String(memberName);
|
||||
capture.buffer = buffer;
|
||||
capture.stride = stride;
|
||||
capture.offset = spvc_compiler_get_member_decoration(compiler, output.base_type_id,
|
||||
member, SpvDecorationOffset);
|
||||
capture.componentCount =
|
||||
XfbComponentCount(compiler, spvc_type_get_member_type(type, member));
|
||||
captures.push_back(Move(capture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the redeclared built-in block ----
|
||||
// SPIRV-Cross keeps gl_PerVertex out of the STAGE_OUTPUT list and reports it here
|
||||
// instead, one entry per built-in member. That is the shape the conformance suite
|
||||
// feeds in first (`layout(xfb_buffer = 0, xfb_offset = 16) out gl_PerVertex { vec4
|
||||
// gl_Position; }`), so walking only the list above would have found nothing at all.
|
||||
const spvc_reflected_builtin_resource* builtins = nullptr;
|
||||
SizeT builtinCount = 0;
|
||||
if (spvc_resources_get_builtin_resource_list_for_type(
|
||||
resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) ==
|
||||
SPVC_SUCCESS) {
|
||||
for (SizeT i = 0; i < builtinCount; ++i) {
|
||||
const spvc_reflected_builtin_resource& entry = builtins[i];
|
||||
const char* name = XfbBuiltInName(entry.builtin);
|
||||
if (name == nullptr) continue;
|
||||
|
||||
Uint32 buffer = 0;
|
||||
Uint32 stride = 0;
|
||||
readVariableDecorations(entry.resource.id, buffer, stride);
|
||||
|
||||
const spvc_type blockType =
|
||||
spvc_compiler_get_type_handle(compiler, entry.resource.base_type_id);
|
||||
if (blockType == nullptr ||
|
||||
spvc_type_get_basetype(blockType) != SPVC_BASETYPE_STRUCT) {
|
||||
continue;
|
||||
}
|
||||
// The member index is not in the reflection entry, so it is recovered by
|
||||
// matching the BuiltIn decoration - the same key the entry is keyed on.
|
||||
const unsigned memberCount = spvc_type_get_num_member_types(blockType);
|
||||
for (unsigned member = 0; member < memberCount; ++member) {
|
||||
if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member,
|
||||
SpvDecorationBuiltIn) != SPVC_TRUE) {
|
||||
continue;
|
||||
}
|
||||
if (spvc_compiler_get_member_decoration(compiler, entry.resource.base_type_id, member,
|
||||
SpvDecorationBuiltIn) !=
|
||||
static_cast<unsigned>(entry.builtin)) {
|
||||
continue;
|
||||
}
|
||||
if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member,
|
||||
SpvDecorationOffset) != SPVC_TRUE) {
|
||||
break; // this built-in is present but not captured
|
||||
}
|
||||
SpirvXfbCapture capture;
|
||||
capture.name = name;
|
||||
capture.buffer = buffer;
|
||||
capture.stride = stride;
|
||||
capture.offset = spvc_compiler_get_member_decoration(
|
||||
compiler, entry.resource.base_type_id, member, SpvDecorationOffset);
|
||||
capture.componentCount =
|
||||
XfbComponentCount(compiler, spvc_type_get_member_type(blockType, member));
|
||||
captures.push_back(Move(capture));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture order IS buffer-then-offset order: that is the order the equivalent
|
||||
// glTransformFeedbackVaryings request has to name them in for the frontend's
|
||||
// packer to reproduce the declared layout.
|
||||
std::stable_sort(captures.begin(), captures.end(),
|
||||
[](const SpirvXfbCapture& a, const SpirvXfbCapture& b) {
|
||||
if (a.buffer != b.buffer) return a.buffer < b.buffer;
|
||||
return a.offset < b.offset;
|
||||
});
|
||||
return captures;
|
||||
}
|
||||
|
||||
void SpvcSession::StripTransformFeedbackDecorations() {
|
||||
if (compiler == nullptr || resources == nullptr) return;
|
||||
|
||||
auto stripVariable = [this](SpvId variableId, spvc_type_id baseTypeId) {
|
||||
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbBuffer);
|
||||
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbStride);
|
||||
spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationOffset);
|
||||
|
||||
const spvc_type type = spvc_compiler_get_type_handle(compiler, baseTypeId);
|
||||
if (type == nullptr || spvc_type_get_basetype(type) != SPVC_BASETYPE_STRUCT) return;
|
||||
const unsigned memberCount = spvc_type_get_num_member_types(type);
|
||||
for (unsigned member = 0; member < memberCount; ++member) {
|
||||
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationOffset);
|
||||
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbBuffer);
|
||||
spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbStride);
|
||||
}
|
||||
};
|
||||
|
||||
const spvc_reflected_resource* outputs = nullptr;
|
||||
SizeT outputCount = 0;
|
||||
if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs,
|
||||
&outputCount) == SPVC_SUCCESS) {
|
||||
for (SizeT i = 0; i < outputCount; ++i) {
|
||||
stripVariable(outputs[i].id, outputs[i].base_type_id);
|
||||
}
|
||||
}
|
||||
const spvc_reflected_builtin_resource* builtins = nullptr;
|
||||
SizeT builtinCount = 0;
|
||||
if (spvc_resources_get_builtin_resource_list_for_type(
|
||||
resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) ==
|
||||
SPVC_SUCCESS) {
|
||||
for (SizeT i = 0; i < builtinCount; ++i) {
|
||||
stripVariable(builtins[i].resource.id, builtins[i].resource.base_type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spvc_result SpvcSession::SetEntryPoint(const char* name, SpvExecutionModel model) {
|
||||
// A null compiler or a null/empty name is a FAILURE, not a silent success: the
|
||||
// caller is asking for a specific entry point and there is none to give it.
|
||||
if (compiler == nullptr || name == nullptr || *name == '\0') return SPVC_ERROR_INVALID_ARGUMENT;
|
||||
return spvc_compiler_set_entry_point(compiler, name, model);
|
||||
}
|
||||
|
||||
Bool SpvcSession::SetSpecializationConstants(const Vector<Uint32>& constantIds,
|
||||
const Vector<Uint32>& constantValues,
|
||||
Uint32& outUnknownConstantId) {
|
||||
if (constantIds.empty()) return true;
|
||||
if (compiler == nullptr) return false;
|
||||
|
||||
const spvc_specialization_constant* declared = nullptr;
|
||||
SizeT declaredCount = 0;
|
||||
if (spvc_compiler_get_specialization_constants(compiler, &declared, &declaredCount) != SPVC_SUCCESS) {
|
||||
outUnknownConstantId = constantIds.front();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (SizeT i = 0; i < constantIds.size(); ++i) {
|
||||
const Uint32 wantedId = constantIds[i];
|
||||
spvc_constant handle = nullptr;
|
||||
for (SizeT j = 0; j < declaredCount; ++j) {
|
||||
if (declared[j].constant_id != wantedId) continue;
|
||||
handle = spvc_compiler_get_constant_handle(compiler, declared[j].id);
|
||||
break;
|
||||
}
|
||||
if (handle == nullptr) {
|
||||
// ARB_gl_spirv: "INVALID_VALUE is generated if any value in pConstantIndex
|
||||
// refers to a specialization constant that does not exist in the shader
|
||||
// module". Reported rather than skipped - a silently ignored id would let
|
||||
// the shader specialize to something the application never asked for.
|
||||
outUnknownConstantId = wantedId;
|
||||
return false;
|
||||
}
|
||||
// The GL side hands over a flat GLuint per constant and ARB_gl_spirv says it
|
||||
// is "interpreted according to the type of the specialization constant", so
|
||||
// the 32-bit PATTERN is what has to be stored, not a converted number.
|
||||
// spvc_constant_set_scalar_u32 writes exactly that pattern into the constant's
|
||||
// scalar union, which SPIRV-Cross then reads back as whatever the constant's
|
||||
// declared type is - the reinterpretation the extension asks for, for free.
|
||||
spvc_constant_set_scalar_u32(handle, 0, 0, constantValues[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
spvc_result SpvcSession::Compile(const char** result) {
|
||||
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
||||
SPVC_CHK_INIT
|
||||
|
||||
@@ -56,6 +56,19 @@ namespace MobileGL {
|
||||
}
|
||||
};
|
||||
|
||||
// One output a SPIR-V module asked to have captured, as its Xfb decorations describe
|
||||
// it. ARB_gl_spirv makes these decorations the ONLY way a SPIR-V program declares
|
||||
// transform feedback - glTransformFeedbackVaryings has no effect on such a program -
|
||||
// so a module that carries them and an implementation that ignores them capture
|
||||
// nothing at all.
|
||||
struct SpirvXfbCapture {
|
||||
String name; // the GL interface name: "gl_Position", or "Block.member"
|
||||
Uint32 buffer = 0; // XfbBuffer on the declaring variable
|
||||
Uint32 offset = 0; // Offset on the variable or on the member
|
||||
Uint32 stride = 0; // XfbStride on the declaring variable
|
||||
Uint32 componentCount = 0; // how many 32-bit components the capture occupies
|
||||
};
|
||||
|
||||
enum class SessionUsageBit {
|
||||
Reflection = 1 << 0,
|
||||
Transpile = 1 << 1,
|
||||
@@ -160,6 +173,43 @@ namespace MobileGL {
|
||||
// A block carrying only ONE of the two is left exactly as it is: those really do
|
||||
// constrain the accesses the shader makes, and the driver is entitled to know.
|
||||
spvc_result RelaxReadWriteExclusiveStorageBuffers();
|
||||
// ---- GL_ARB_gl_spirv: an APPLICATION-supplied module, not one MobileGL emitted ----
|
||||
// Select which OpEntryPoint of `model` this session compiles. A module may carry
|
||||
// several of the same execution model, and glSpecializeShader names the one the
|
||||
// shader object stands for.
|
||||
// Whether the transpile constructor actually built a compiler. Every SPIRV-Cross
|
||||
// handle below is default-null and the C API leaves its out-params untouched on
|
||||
// failure, so a module SPIRV-Cross cannot parse used to leave `ir` null and then
|
||||
// have spvc_context_create_compiler dereference it - a raw null read that
|
||||
// SPVC_BEGIN_SAFE_SCOPE cannot catch. Only glShaderBinary feeds this class bytes
|
||||
// MobileGL did not generate itself, which is why the check earns its keep now.
|
||||
Bool IsTranspileReady() const { return compiler != nullptr && resources != nullptr; }
|
||||
// Read the module's transform-feedback layout out of its Xfb decorations, in
|
||||
// (buffer, offset) order. Empty when the module declares no capture.
|
||||
Vector<SpirvXfbCapture> ReflectTransformFeedbackCaptures() const;
|
||||
// Remove every Xfb decoration the reflection above just read.
|
||||
//
|
||||
// This is not tidying: the decorations must not survive into the GLSL this session
|
||||
// emits. SPIRV-Cross re-emits them as `layout(xfb_buffer = N, xfb_stride = M) out
|
||||
// gl_PerVertex { layout(xfb_offset = K) ... }`, glslang re-encodes that into the
|
||||
// regenerated SPIR-V, and the DirectGLES leg then transpiles THAT to ESSL - where
|
||||
// the same SPIRV-Cross throws "Need GL_ARB_enhanced_layouts for xfb_stride or
|
||||
// xfb_buffer" and the stage silently fails to build, leaving a program that links
|
||||
// clean and draws nothing. Stripping them and re-declaring the capture through
|
||||
// MobileGL's ordinary capture machinery (which both backends already implement)
|
||||
// routes a SPIR-V program down exactly the path a GLSL program takes.
|
||||
void StripTransformFeedbackDecorations();
|
||||
spvc_result SetEntryPoint(const char* name, SpvExecutionModel model);
|
||||
// Bake glSpecializeShader's values into the module's specialization constants.
|
||||
// Every value is a GLuint on the GL side and is reinterpreted according to the
|
||||
// constant's own scalar type, exactly as ARB_gl_spirv specifies ("the value is
|
||||
// interpreted as the type of the specialization constant"). Returns false and
|
||||
// sets `outUnknownConstantId` when an id the caller passed is not a
|
||||
// specialization constant of this module, which the extension makes
|
||||
// GL_INVALID_VALUE.
|
||||
Bool SetSpecializationConstants(const Vector<Uint32>& constantIds,
|
||||
const Vector<Uint32>& constantValues,
|
||||
Uint32& outUnknownConstantId);
|
||||
spvc_result Compile(const char** result);
|
||||
const SpvcMetadata& GetMetadata() const;
|
||||
const char* GetLastErrorString() const;
|
||||
|
||||
@@ -14,6 +14,20 @@ namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
|
||||
// The default-block uniform InjectNumSamplesBuiltinShim declares to stand in for the
|
||||
// gl_NumSamples built-in, which glslang does not put in the symbol table under a
|
||||
// SPIR-V target (Initialize.cpp guards both the desktop and the ES declaration on
|
||||
// `spvVersion.spv == 0`, and MobileGL always targets SPIR-V). The relaxed parse folds
|
||||
// it into GLOBAL_UBO_NAME like any other default-block uniform, which is what lets
|
||||
// BOTH backends pick the value up from the one buffer they already upload; the link
|
||||
// task keeps it out of the GL-visible uniform surface, and the draw path writes the
|
||||
// current draw framebuffer's sample count into it.
|
||||
//
|
||||
// RESERVED, not merely conventional: a shader that declares this name itself keeps
|
||||
// the shim from firing (the injector bails on it), but if it declares the name AND
|
||||
// uses gl_NumSamples the link task will still hide its uniform. That is the same
|
||||
// bargain every mg_-prefixed rewrite in this pipeline strikes.
|
||||
inline const char* NUM_SAMPLES_UNIFORM_NAME = "mg_NumSamples";
|
||||
// glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a
|
||||
// synthesized storage block named "<this>_<GL atomic-counter binding>"
|
||||
// (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter
|
||||
@@ -66,6 +80,53 @@ namespace MobileGL {
|
||||
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
|
||||
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
|
||||
|
||||
// ---- Tessellation per-stage resource limits ----
|
||||
//
|
||||
// Here for exactly the reason the atomic-counter block above is here. GL 4.6 requires
|
||||
// glGetIntegerv and the matching gl_MaxTess* built-in constant to report the same
|
||||
// number (KHR-GL45.limits.max_tess_* reads the query and then compiles a shader that
|
||||
// writes the built-in into an SSBO and demands equality), and these numbers used to
|
||||
// exist ONLY inside BuildTBuiltInResource - so gl_MaxTessControlInputComponents
|
||||
// compiled fine while glGetIntegerv of the same limit had no case at all and answered
|
||||
// GL_INVALID_ENUM. Never move one of these without the other.
|
||||
//
|
||||
// The values are the GL 4.6 core minimums (table 23.55), which is what a frontend that
|
||||
// synthesizes the tessellation stages onto ES/Vulkan can honestly promise.
|
||||
inline constexpr Int MAX_TESS_CONTROL_INPUT_COMPONENTS = 128;
|
||||
inline constexpr Int MAX_TESS_CONTROL_OUTPUT_COMPONENTS = 128;
|
||||
inline constexpr Int MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS = 16;
|
||||
inline constexpr Int MAX_TESS_CONTROL_UNIFORM_COMPONENTS = 1024;
|
||||
inline constexpr Int MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS = 4096;
|
||||
inline constexpr Int MAX_TESS_EVALUATION_INPUT_COMPONENTS = 128;
|
||||
inline constexpr Int MAX_TESS_EVALUATION_OUTPUT_COMPONENTS = 128;
|
||||
inline constexpr Int MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS = 16;
|
||||
inline constexpr Int MAX_TESS_EVALUATION_UNIFORM_COMPONENTS = 1024;
|
||||
inline constexpr Int MAX_TESS_PATCH_COMPONENTS = 120;
|
||||
|
||||
// ---- Varying and default-block uniform capacities ----
|
||||
//
|
||||
// The *_VECTORS limits are the *_COMPONENTS ones counted in vec4s, so they are DERIVED
|
||||
// rather than typed independently: GL_MAX_VARYING_COMPONENTS said 64 while
|
||||
// GL_MAX_VARYING_VECTORS said 8, and GL_MAX_VERTEX_UNIFORM_COMPONENTS said 4096 while
|
||||
// GL_MAX_VERTEX_UNIFORM_VECTORS said 128 - two pairs that cannot both describe the
|
||||
// same capacity, and both *_VECTORS answers were below the GL 4.5 core minimum
|
||||
// (15 and 256 respectively). Shared with BuildTBuiltInResource because
|
||||
// gl_MaxVaryingVectors and gl_MaxVertexUniformVectors expand from the same numbers.
|
||||
inline constexpr Int MAX_VARYING_COMPONENTS = 64;
|
||||
inline constexpr Int MAX_VARYING_VECTORS = MAX_VARYING_COMPONENTS / 4;
|
||||
inline constexpr Int MAX_VERTEX_UNIFORM_COMPONENTS = 4096;
|
||||
inline constexpr Int MAX_VERTEX_UNIFORM_VECTORS = MAX_VERTEX_UNIFORM_COMPONENTS / 4;
|
||||
|
||||
// GL 4.6 core table 23.53 sets the GL_MAX_SAMPLES minimum at 4, and MobileGL floors
|
||||
// the backend's answer at it (GL_Getter's GetAdvertisedMaxSamples). gl_MaxSamples has
|
||||
// to expand to the SAME number - it is also what sizes gl_SampleMask[] /
|
||||
// gl_SampleMaskIn[] and what bounds a constant index into them - so the floor lives
|
||||
// here and both sides apply it. NOTE the deliberate asymmetry: only MAX_SAMPLES has a
|
||||
// floor of 4. MAX_INTEGER_SAMPLES, MAX_COLOR_TEXTURE_SAMPLES and
|
||||
// MAX_DEPTH_TEXTURE_SAMPLES have a minimum of ONE in the same table and are reported
|
||||
// as the backend probed them.
|
||||
inline constexpr Int MIN_ADVERTISED_MAX_SAMPLES = 4;
|
||||
|
||||
struct EmptyType {};
|
||||
|
||||
enum class ShaderCompileBits : Uint {
|
||||
@@ -84,6 +145,33 @@ namespace MobileGL {
|
||||
const CompileEnv* env = nullptr;
|
||||
};
|
||||
|
||||
// The per-device ceilings a shader-declared `layout(binding = N)` is measured
|
||||
// against - one per resource kind, because GL gives each kind its own limit and they
|
||||
// differ by an order of magnitude on real hardware (a Mali-G925 reports 96 combined
|
||||
// texture image units and 21 image units).
|
||||
//
|
||||
// These exist because glslang cannot enforce them for MobileGL. It owns ceilings for
|
||||
// samplers/images and for atomic counters, and both are switched OFF by the parse
|
||||
// configuration MobileGL uses everywhere - `spvVersion.vulkan == 0` gates the first
|
||||
// and `!spvVersion.vulkanRelaxed` the second (ParseHelper.cpp layoutTypeCheck), and
|
||||
// MobileGL always parses with setEnvClient(EShClientVulkan) +
|
||||
// setEnvInputVulkanRulesRelaxed(). For uniform and storage BLOCKS glslang quotes the
|
||||
// spec sentence and then checks nothing at all. Flipping to the OpenGL client to wake
|
||||
// those checks is not an option (it would change the parse the whole relaxed
|
||||
// lowering pipeline is built on) and would not even be correct: glslang measures
|
||||
// IMAGE bindings against the SAMPLER limit and hardcodes that limit at 80, so it
|
||||
// would reject legal bindings 80..95 and keep under-rejecting images.
|
||||
//
|
||||
// Zero or negative means "no ceiling to enforce for this kind" - a backendless
|
||||
// environment, which every unit test and the pre-init preload path run in.
|
||||
struct ResourceBindingLimits {
|
||||
Int MaxSamplerBindings = 0; // GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS
|
||||
Int MaxImageBindings = 0; // GL_MAX_IMAGE_UNITS
|
||||
Int MaxUniformBufferBindings = 0; // GL_MAX_UNIFORM_BUFFER_BINDINGS
|
||||
Int MaxShaderStorageBufferBindings = 0; // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
|
||||
Int MaxAtomicCounterBufferBindings = 0; // GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS
|
||||
};
|
||||
|
||||
struct ProgramAttrib {
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
UnorderedMap<String, Uint> explicitVertexInLocations;
|
||||
@@ -99,6 +187,11 @@ namespace MobileGL {
|
||||
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr;
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr;
|
||||
// IN: the ceilings above. OUT: the first violation the resolver found, in the
|
||||
// same capture window and for the same reason - past mapIO's doMap() every
|
||||
// resource carries an ASSIGNED binding and the question can no longer be asked.
|
||||
ResourceBindingLimits resourceBindingLimits{};
|
||||
String* resourceBindingViolation = nullptr;
|
||||
};
|
||||
|
||||
struct ProgramBinaryAttrib {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "TMglGlslIoResolver.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
@@ -165,6 +167,101 @@ namespace MobileGL {
|
||||
// before the preprocessor's macros were expanded and therefore could not read
|
||||
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
|
||||
// its storage blocks. Asking the AST instead makes the macro case ordinary.
|
||||
// GLSL 4.30 4.4.5 and ES 3.1 4.4.4: `layout(binding = N)` on any opaque uniform, uniform
|
||||
// block, storage block or atomic counter is a COMPILE-TIME error when N is not less than that
|
||||
// resource kind's implementation limit - and, for an ARRAY of them, when base + count - 1 is
|
||||
// not. MobileGL enforces it here rather than at compile because here is the last point where
|
||||
// `qualifier.hasBinding()` still means "the SHADER said so" (see the comment on the caller),
|
||||
// and because the per-device ceilings are deliberately not part of the compile pipeline's
|
||||
// memo keys. The conformance suite accepts a link-time rejection: its predicate is
|
||||
// compiledAndLinked(), which is the AND of the two.
|
||||
//
|
||||
// FIVE KINDS HERE, AND ONE OF THEM IS ALSO CHECKED EARLIER. Before this, exactly one kind -
|
||||
// shader-storage blocks - was checked at all, by a bespoke lexical scan of the shader source,
|
||||
// which is why the storage sub-family was the one that passed while sampler, image,
|
||||
// uniform-block and atomic-counter bindings sailed past every ceiling.
|
||||
//
|
||||
// That scan is deliberately KEPT (ShaderCompileTask.cpp's MaxShaderStorageBufferBindings
|
||||
// explains why: GLSL makes an over-range binding a COMPILE-time error, and the relaxed Vulkan
|
||||
// parse leaves the scan as the only place MobileGL can raise one). So the storage arm has two
|
||||
// enforcement points and the other four have this one. What keeps them from drifting is not
|
||||
// that there is only one site but that both read the SAME numbers - ResolveResourceBindingLimits
|
||||
// is the single derivation, and neither site computes a ceiling of its own.
|
||||
void TMglGlslIoResolver::CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name) {
|
||||
if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return;
|
||||
if (!m_bindingViolation->empty()) return; // first violation wins; the link is already lost
|
||||
|
||||
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||
const char* kind = nullptr;
|
||||
const char* limitName = nullptr;
|
||||
Int limit = 0;
|
||||
long long binding = -1;
|
||||
|
||||
if (type.getBasicType() == glslang::EbtSampler && qualifier.hasBinding()) {
|
||||
const bool isImage = type.getSampler().isImage();
|
||||
kind = isImage ? "image" : "sampler";
|
||||
limitName = isImage ? "GL_MAX_IMAGE_UNITS" : "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS";
|
||||
limit = isImage ? m_bindingLimits->MaxImageBindings : m_bindingLimits->MaxSamplerBindings;
|
||||
binding = qualifier.layoutBinding;
|
||||
} else if (type.getBasicType() == glslang::EbtBlock) {
|
||||
// An atomic counter never reaches here as a counter: the relaxed parse has already
|
||||
// folded it into a synthesized "gl_AtomicCounterBlock_<binding>" storage block whose
|
||||
// TRAILING NUMBER is the GL binding the shader asked for (ParseContextBase::
|
||||
// growAtomicCounterBlock names it from bufferBinding). That name is the only surviving
|
||||
// record of the declaration, so it is what the counter ceiling is read off.
|
||||
const Int counterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(
|
||||
StringView(name.c_str(), name.size()));
|
||||
if (counterBinding >= 0) {
|
||||
kind = "atomic_uint";
|
||||
limitName = "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS";
|
||||
limit = m_bindingLimits->MaxAtomicCounterBufferBindings;
|
||||
binding = counterBinding;
|
||||
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqUniform &&
|
||||
name.compare(MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != 0) {
|
||||
kind = "uniform block";
|
||||
limitName = "GL_MAX_UNIFORM_BUFFER_BINDINGS";
|
||||
limit = m_bindingLimits->MaxUniformBufferBindings;
|
||||
binding = qualifier.layoutBinding;
|
||||
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqBuffer) {
|
||||
kind = "buffer block";
|
||||
limitName = "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS";
|
||||
limit = m_bindingLimits->MaxShaderStorageBufferBindings;
|
||||
binding = qualifier.layoutBinding;
|
||||
}
|
||||
}
|
||||
|
||||
if (kind == nullptr || limit <= 0 || binding < 0) return;
|
||||
|
||||
// The ARRAYED-INSTANCE rule: an array of N takes bindings base .. base + N - 1, and every
|
||||
// one of them has to fit. getCumulativeArraySize() folds a multi-dimensional array into
|
||||
// the count of leaf elements, which is exactly how many consecutive bindings GL hands out.
|
||||
//
|
||||
// isSizedArray() is MANDATORY, not defensive. glslang's TArraySizes::getCumulativeSize()
|
||||
// asserts `sizes.getDimSize(d) != UnsizedArraySize` ("this only makes sense in paths that
|
||||
// have a known array size"), so calling it on a run-time-sized array - the ordinary shape
|
||||
// of a storage block's trailing member, and legal on the block instance itself - aborts
|
||||
// the process inside mapIO's collect callback in any build with assertions live. The
|
||||
// repo defines no NDEBUG of its own, so a CMake Debug build is exactly such a build; the
|
||||
// "reports 0" behaviour the previous comment relied on is only what NDEBUG happens to do.
|
||||
// An unsized array occupies one binding here, which is also what GL means by it.
|
||||
long long elementCount = 1;
|
||||
if (type.isArray() && type.isSizedArray()) {
|
||||
const int cumulative = static_cast<int>(type.getCumulativeArraySize());
|
||||
if (cumulative > 1) elementCount = cumulative;
|
||||
}
|
||||
const long long lastBinding = binding + elementCount - 1;
|
||||
if (lastBinding < static_cast<long long>(limit)) return;
|
||||
|
||||
String message = "Error: layout(binding = " + std::to_string(binding) + ") on " + kind + " '" +
|
||||
String(name.c_str()) + "'";
|
||||
if (elementCount > 1) {
|
||||
message += " (an array of " + std::to_string(elementCount) + ", occupying bindings " +
|
||||
std::to_string(binding) + ".." + std::to_string(lastBinding) + ")";
|
||||
}
|
||||
message += " is not less than " + String(limitName) + " (" + std::to_string(limit) + ").";
|
||||
*m_bindingViolation = Move(message);
|
||||
}
|
||||
|
||||
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||
const glslang::TType& type = ent.symbol->getType();
|
||||
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||
@@ -207,6 +304,8 @@ namespace MobileGL {
|
||||
m_uniformBlocksWithoutBinding->insert(name.c_str());
|
||||
}
|
||||
|
||||
CheckDeclaredBindingRange(type, name);
|
||||
|
||||
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,27 +21,35 @@
|
||||
#include <glslang/MachineIndependent/iomapper.h>
|
||||
#include "TVarEntryInfo.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
|
||||
namespace MobileGL {
|
||||
class TMglGlslIoResolver : public glslang::TDefaultGlslIoResolver {
|
||||
public:
|
||||
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
|
||||
using ResourceBindingLimits = MG_Util::ShaderTranspiler::ResourceBindingLimits;
|
||||
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
|
||||
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
|
||||
ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr,
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr)
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr,
|
||||
const ResourceBindingLimits* bindingLimits = nullptr,
|
||||
String* bindingViolation = nullptr)
|
||||
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
|
||||
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
|
||||
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding),
|
||||
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding) {}
|
||||
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding), m_bindingLimits(bindingLimits),
|
||||
m_bindingViolation(bindingViolation) {}
|
||||
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
|
||||
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
|
||||
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
|
||||
std::set<String>* storageBlocksWithoutBinding = nullptr,
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr)
|
||||
std::set<String>* uniformBlocksWithoutBinding = nullptr,
|
||||
const ResourceBindingLimits* bindingLimits = nullptr,
|
||||
String* bindingViolation = nullptr)
|
||||
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
|
||||
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding) {}
|
||||
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding,
|
||||
bindingLimits, bindingViolation) {}
|
||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||
@@ -72,6 +80,10 @@ namespace MobileGL {
|
||||
// resource kind on set 0), so an unbound block declared after an unbound image lands on
|
||||
// 1. See ProgramLinkTask's UBO reflection loop for what is done with them.
|
||||
std::set<String>* m_uniformBlocksWithoutBinding = nullptr;
|
||||
// The binding-range rule, IN and OUT. See RecordBindingRangeViolation.
|
||||
const ResourceBindingLimits* m_bindingLimits = nullptr;
|
||||
String* m_bindingViolation = nullptr;
|
||||
void CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name);
|
||||
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||
bool m_plainUniformLocationsAssigned = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user