mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0373eb48 | ||
|
|
7480bf4490 | ||
|
|
54b206d90c | ||
|
|
5daf7bf093 | ||
|
|
a8228ca287 | ||
|
|
8b827bd2ce | ||
|
|
6aa161fee7 | ||
|
|
48a70fea81 | ||
|
|
6ea4f32635 | ||
|
|
dc1fffb041 | ||
|
|
d24d5b5ccd | ||
|
|
51883cf1a3 | ||
|
|
6dfadeb7d2 | ||
|
|
4fc3531d0d | ||
|
|
9bde0e500f | ||
|
|
3477d87b50 | ||
|
|
c2a081fa75 | ||
|
|
685fd750c9 | ||
|
|
02c9b8a32d | ||
|
|
26f02567d7 | ||
|
|
a3dbe234d7 | ||
|
|
de8e7a4606 | ||
|
|
6359fba455 | ||
|
|
872876961d | ||
|
|
e2923a239f | ||
|
|
1740a8a41a | ||
|
|
6b1d89f279 | ||
|
|
01fbe0b4b0 | ||
|
|
cb155c5b94 | ||
|
|
04a06438c5 | ||
|
|
db00774224 | ||
|
|
f378c1a064 | ||
|
|
421ccd08c6 | ||
|
|
039af520bf | ||
|
|
cdba7bed2e | ||
|
|
1eeeb44d94 | ||
|
|
fa2e15c27e | ||
|
|
14744f117c | ||
|
|
8329ab4264 | ||
|
|
ee98c453ed | ||
|
|
f88322ce84 | ||
|
|
93f1106ba4 | ||
|
|
31b5b563d6 | ||
|
|
a9fb7ef0af | ||
|
|
6159166d38 | ||
|
|
5fecfa42f6 | ||
|
|
d48e5d0053 | ||
|
|
7a0182b58f | ||
|
|
0f523db14d | ||
|
|
442cec1a15 | ||
|
|
246a438138 |
@@ -270,6 +270,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
@@ -282,6 +283,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||||
@@ -388,6 +390,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
|
|||||||
@@ -203,6 +203,15 @@ namespace MobileGL::MG_Config {
|
|||||||
// immediately stay serial by their own construction). Off by default; never
|
// immediately stay serial by their own construction). Off by default; never
|
||||||
// advertise it.
|
// advertise it.
|
||||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
|
||||||
|
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
|
||||||
|
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
|
||||||
|
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
|
||||||
|
// ON; ForceOff turns ALL THREE off and makes every translation run from
|
||||||
|
// scratch. The escape hatch exists because a wrong cache hit is a silently
|
||||||
|
// miscompiled shader: if a device ever renders differently with the cache
|
||||||
|
// on, one run with this falsy says so.
|
||||||
|
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
features.AsyncOptimisticShaderStatus =
|
features.AsyncOptimisticShaderStatus =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
|
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -72,6 +74,14 @@ namespace MobileGL {
|
|||||||
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||||
// make the next Initialize() skip a prewarm it genuinely needs.
|
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||||
|
// The two-level translation memo. Nothing in it references a glslang object -
|
||||||
|
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
|
||||||
|
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
|
||||||
|
// fordebug build gets one line per level saying how the run went.
|
||||||
|
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
|
||||||
|
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
|
||||||
|
MG_State::GLState::LogProgramTranslationCacheStats();
|
||||||
|
MG_State::GLState::ClearProgramTranslationCache();
|
||||||
MG_Backend::gBackendFunctionsTable = {};
|
MG_Backend::gBackendFunctionsTable = {};
|
||||||
g_isInitialized = false;
|
g_isInitialized = false;
|
||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
|
|||||||
@@ -372,8 +372,32 @@ namespace MobileGL {
|
|||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
|
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
||||||
|
// backend that cannot host a clip distance MUST report it: advertising eight the
|
||||||
|
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
||||||
|
// from an honest "unsupported" at query time to a backend shader-compile error the
|
||||||
|
// frontend never surfaces, after which every draw with that program silently renders
|
||||||
|
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
||||||
|
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
||||||
|
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
||||||
|
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||||
|
// hand glslang a workable gl_MaxClipDistances.
|
||||||
Int MaxClipDistances = 8;
|
Int MaxClipDistances = 8;
|
||||||
Int MaxViewports = 16;
|
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
|
||||||
|
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
|
||||||
|
// a convention is a statement about behaviour, so a backend that does not pin one
|
||||||
|
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
|
||||||
|
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
|
||||||
|
// capability is absent: without the viewport array extension only viewport 0 is ever
|
||||||
|
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
|
||||||
|
// both - which vertex provokes is decided per pipeline by
|
||||||
|
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
||||||
|
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
||||||
|
// of the backend.
|
||||||
|
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
Int MaxViewportWidth = 16384;
|
Int MaxViewportWidth = 16384;
|
||||||
Int MaxViewportHeight = 16384;
|
Int MaxViewportHeight = 16384;
|
||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
|
|||||||
@@ -213,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
}
|
}
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
// A format is either 8- or 16-bit signed normalized, so at most one of the two ever
|
||||||
|
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
|
||||||
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
reasons.push_back("EXT_render_snorm not supported");
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1336,6 +1339,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
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
|
||||||
|
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
|
||||||
|
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
|
||||||
|
// describe behaviour this backend does not implement.
|
||||||
|
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
|
||||||
|
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
|
||||||
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||||
|
|||||||
@@ -395,6 +395,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
const SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(BufferTarget::AtomicCounter);
|
||||||
|
for (const Int glBinding : glBindings) {
|
||||||
|
if (glBinding < 0 || static_cast<SizeT>(glBinding) >= pointCount) continue;
|
||||||
|
const Int esslBinding = esslBindingTop - glBinding;
|
||||||
|
// Already diagnosed once when the block was transpiled; nothing was bound to it
|
||||||
|
// there either, so there is nothing to unbind here.
|
||||||
|
if (esslBinding < 0) continue;
|
||||||
|
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter,
|
||||||
|
static_cast<Uint>(glBinding));
|
||||||
|
auto& obj = point.GetBoundObject();
|
||||||
|
if (!obj) {
|
||||||
|
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* backendResource = EnsureBufferResource(obj);
|
||||||
|
if (!backendResource || backendResource->id == 0) {
|
||||||
|
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& range = point.GetRange();
|
||||||
|
if (range.start == 0 && range.end >= obj->GetSize()) {
|
||||||
|
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
|
||||||
|
backendResource->id);
|
||||||
|
} else {
|
||||||
|
const auto start = std::min(range.start, obj->GetSize());
|
||||||
|
const auto end = std::min(range.end, obj->GetSize());
|
||||||
|
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding),
|
||||||
|
backendResource->id, static_cast<GLintptr>(start),
|
||||||
|
static_cast<GLsizeiptr>(end - start));
|
||||||
|
}
|
||||||
|
// The whole point of a counter is that the shader INCREMENTS it, and every
|
||||||
|
// conformance case reads the result back with glMapBufferRange or
|
||||||
|
// glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is
|
||||||
|
// flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer.
|
||||||
|
obj->MarkGpuWritten();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
|
void SyncBoundBuffer(BufferTarget target, GLenum glTarget) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -1394,12 +1438,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highest image unit that has ever been given a texture, plus one. Maintained by the
|
||||||
|
// single funnel below, so it is a sound "no draw in this context can be reading an image"
|
||||||
|
// test: nothing reaches an image unit without going through SyncImageTextureBinding.
|
||||||
|
// Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the
|
||||||
|
// draw-path staleness check below at one integer test.
|
||||||
|
static Uint g_imageUnitHighWaterMark = 0;
|
||||||
|
|
||||||
void SyncImageTextureBinding(Uint unit) {
|
void SyncImageTextureBinding(Uint unit) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit));
|
||||||
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
|
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
|
||||||
|
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
|
||||||
|
g_imageUnitHighWaterMark = unit + 1;
|
||||||
|
}
|
||||||
if (!imageBinding.Texture) {
|
if (!imageBinding.Texture) {
|
||||||
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
return;
|
return;
|
||||||
@@ -1453,6 +1507,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SyncImageTextureBinding(unit);
|
SyncImageTextureBinding(unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the draw path last swept the image units against. A draw never swept them at all:
|
||||||
|
// an image unit was established once, eagerly, by glBindImageTexture and never revisited.
|
||||||
|
// That is stale the moment the texture behind it is re-specified with a new size or
|
||||||
|
// format, because ES 3.1 only allows IMMUTABLE storage on an image unit
|
||||||
|
// (SyncTextureObjectToBackend's imageBindableStorageRequired), immutable storage cannot be
|
||||||
|
// redefined, and so the re-spec MINTS A NEW ES TEXTURE NAME - leaving the unit pointing at
|
||||||
|
// the deleted one and imageSize() reporting the old dimensions
|
||||||
|
// (KHR-GL43.shader_image_size.advanced-changeSize).
|
||||||
|
static Uint64 g_imageSweepContextId = 0;
|
||||||
|
static Uint64 g_imageSweepSamplingGeneration = 0;
|
||||||
|
static Uint g_imageSweepBackendContextGeneration = 0;
|
||||||
|
static Bool g_imageSweepValid = false;
|
||||||
|
|
||||||
|
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
|
||||||
|
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
|
||||||
|
// moves on exactly the shape and format changes that can force the re-mint. Deliberately
|
||||||
|
// NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be
|
||||||
|
// the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside
|
||||||
|
// this very sweep, so a backend-side trigger would be bumped after the gate had already
|
||||||
|
// declined to run it.
|
||||||
|
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
|
||||||
|
if (g_imageUnitHighWaterMark == 0) return;
|
||||||
|
if (g_imageSweepValid && g_imageSweepContextId == keys.contextId &&
|
||||||
|
g_imageSweepSamplingGeneration == keys.samplingGeneration &&
|
||||||
|
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SyncImageTextureBindings();
|
||||||
|
g_imageSweepContextId = keys.contextId;
|
||||||
|
g_imageSweepSamplingGeneration = keys.samplingGeneration;
|
||||||
|
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
|
||||||
|
g_imageSweepValid = true;
|
||||||
|
}
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {
|
namespace FramebufferImpl {
|
||||||
@@ -2396,6 +2484,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
syncBit & DrawSyncBit::IndirectBuffer);
|
syncBit & DrawSyncBit::IndirectBuffer);
|
||||||
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
||||||
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
||||||
|
// A draw reads and writes through its image units too, so the unit bindings have to be
|
||||||
|
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
|
||||||
|
// pays one integer test, and one with images re-issues them only when a texture shape
|
||||||
|
// moved under them.
|
||||||
|
TextureImpl::SyncImageTextureBindingsForDraw(textureKeys);
|
||||||
// A draw writes through its image units too - the conformance case that found this
|
// A draw writes through its image units too - the conformance case that found this
|
||||||
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
|
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
|
||||||
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
||||||
@@ -2915,6 +3008,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
|
||||||
|
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
|
||||||
|
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
|
||||||
|
// program declares, and no other program's blocks live there. Both the draw and
|
||||||
|
// the dispatch path reach this, which is what a compute-shader counter needs.
|
||||||
|
if (!backendProgram.GetAtomicCounterBindings().empty()) {
|
||||||
|
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
|
||||||
|
backendProgram.GetAtomicCounterEsslBindingTop());
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -5683,15 +5786,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_GLESFuncs.glDispatchComputeIndirect(indirect);
|
g_GLESFuncs.glDispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An atomic counter is a shader storage block by the time it reaches the ES driver (glslang
|
||||||
|
// lowers every atomic_uint onto one), so an application that asks only for the counter
|
||||||
|
// barrier is asking about memory the driver knows as storage-buffer memory. Ordering one
|
||||||
|
// does not oblige a driver to order the other, so the counter bit implies the storage bit
|
||||||
|
// here - which is what the lowering costs and the only place it can be paid.
|
||||||
|
static GLbitfield LowerAtomicCounterBarrierBits(GLbitfield barriers) {
|
||||||
|
if ((barriers & GL_ATOMIC_COUNTER_BARRIER_BIT) != 0) {
|
||||||
|
barriers |= GL_SHADER_STORAGE_BARRIER_BIT;
|
||||||
|
}
|
||||||
|
return barriers;
|
||||||
|
}
|
||||||
|
|
||||||
void MemoryBarrier(GLbitfield barriers) {
|
void MemoryBarrier(GLbitfield barriers) {
|
||||||
g_GLESFuncs.glMemoryBarrier(barriers);
|
g_GLESFuncs.glMemoryBarrier(LowerAtomicCounterBarrierBits(barriers));
|
||||||
if (g_GLESCapabilities.IsAngleRenderer) {
|
if (g_GLESCapabilities.IsAngleRenderer) {
|
||||||
g_GLESFuncs.glFlush();
|
g_GLESFuncs.glFlush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemoryBarrierByRegion(GLbitfield barriers) {
|
void MemoryBarrierByRegion(GLbitfield barriers) {
|
||||||
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
|
g_GLESFuncs.glMemoryBarrierByRegion(LowerAtomicCounterBarrierBits(barriers));
|
||||||
}
|
}
|
||||||
|
|
||||||
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
||||||
@@ -7569,6 +7684,149 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Bit-exact readback of a 32-bit packed colour level ---------------------------------------
|
||||||
|
//
|
||||||
|
// glGetTexImage of a packed format read with its OWN client type owes the application the words
|
||||||
|
// the image HOLDS, and neither of the two routes above can promise that once anything other than
|
||||||
|
// a glTexImage has written the level:
|
||||||
|
//
|
||||||
|
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
||||||
|
// RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and
|
||||||
|
// collapses an R11F_G11F_B10F NaN to the canonical payload 1
|
||||||
|
// (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly
|
||||||
|
// such a NaN in the 10-bit blue field every time, because the source's shared-exponent
|
||||||
|
// field is all ones;
|
||||||
|
// * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote
|
||||||
|
// it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that
|
||||||
|
// up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer
|
||||||
|
// source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a
|
||||||
|
// self-copy - and the decline is silent, so the stale words are served as truth.
|
||||||
|
//
|
||||||
|
// glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour
|
||||||
|
// format in one compatibility class, so copying the level into a scratch GL_R32UI image and
|
||||||
|
// reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote
|
||||||
|
// them. This is what lets the shadow stop being the authority for these formats: it is tried
|
||||||
|
// first, and every step reports rather than guesses, so a driver that turns any of it down
|
||||||
|
// simply leaves the old shadow/attachment fallbacks to run.
|
||||||
|
static GLuint g_packedWordScratchTextureId = 0;
|
||||||
|
static GLsizei g_packedWordScratchWidth = 0;
|
||||||
|
static GLsizei g_packedWordScratchHeight = 0;
|
||||||
|
|
||||||
|
// Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused
|
||||||
|
// the storage, which is a decline and not an error.
|
||||||
|
static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) {
|
||||||
|
if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width &&
|
||||||
|
g_packedWordScratchHeight >= height) {
|
||||||
|
return g_packedWordScratchTextureId;
|
||||||
|
}
|
||||||
|
const GLsizei newWidth = std::max(width, g_packedWordScratchWidth);
|
||||||
|
const GLsizei newHeight = std::max(height, g_packedWordScratchHeight);
|
||||||
|
if (g_packedWordScratchTextureId != 0) {
|
||||||
|
// A scratch FBO may still name the old id, and the driver is free to hand the same
|
||||||
|
// number back for the replacement - which would false-skip the re-attach.
|
||||||
|
ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId);
|
||||||
|
g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId);
|
||||||
|
g_packedWordScratchTextureId = 0;
|
||||||
|
g_packedWordScratchWidth = 0;
|
||||||
|
g_packedWordScratchHeight = 0;
|
||||||
|
}
|
||||||
|
GLuint texture = 0;
|
||||||
|
g_GLESFuncs.glGenTextures(1, &texture);
|
||||||
|
if (texture == 0) return 0;
|
||||||
|
|
||||||
|
ClearGLErrors();
|
||||||
|
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
||||||
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
// Immutable single-level storage: glCopyImageSubData wants a complete image, and
|
||||||
|
// glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete
|
||||||
|
// under the default mipmapping filter.
|
||||||
|
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight);
|
||||||
|
const GLenum storageError = g_GLESFuncs.glGetError();
|
||||||
|
// Re-bind whatever the binding cache says lives on the temp unit, so the cache stays
|
||||||
|
// truthful without a driver query (same discipline as CopyR32FTexture2D).
|
||||||
|
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
||||||
|
[static_cast<SizeT>(TextureTarget::Texture2D)];
|
||||||
|
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
||||||
|
if (storageError != GL_NO_ERROR) {
|
||||||
|
g_GLESFuncs.glDeleteTextures(1, &texture);
|
||||||
|
MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable",
|
||||||
|
newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
g_packedWordScratchTextureId = texture;
|
||||||
|
g_packedWordScratchWidth = newWidth;
|
||||||
|
g_packedWordScratchHeight = newHeight;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ReleasePackedWordScratchTexture() {
|
||||||
|
// The ES context (and the name with it) is gone; deleting here would target a recycled
|
||||||
|
// name in the successor context.
|
||||||
|
g_packedWordScratchTextureId = 0;
|
||||||
|
g_packedWordScratchWidth = 0;
|
||||||
|
g_packedWordScratchHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`.
|
||||||
|
static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice,
|
||||||
|
GLsizei width, GLsizei height, Uint32* outWords) {
|
||||||
|
if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false;
|
||||||
|
if (!g_GLESFuncs.glCopyImageSubData) return false;
|
||||||
|
|
||||||
|
// Horizontal bands, so neither the scratch image nor the staging buffer scales with the
|
||||||
|
// level. The scratch is grow-only on purpose - a sweep down a mip chain must not
|
||||||
|
// reallocate per level - which without a band cap would leave a 4096x4096 readback's
|
||||||
|
// 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with
|
||||||
|
// 4 MiB of staging behind it because the read lands four words per texel.
|
||||||
|
constexpr SizeT kMaxScratchTexels = SizeT{1} << 18;
|
||||||
|
const GLsizei bandRows = std::max<GLsizei>(
|
||||||
|
1, static_cast<GLsizei>(std::min<SizeT>(kMaxScratchTexels / static_cast<SizeT>(width),
|
||||||
|
static_cast<SizeT>(height))));
|
||||||
|
const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows);
|
||||||
|
if (scratch == 0) return false;
|
||||||
|
|
||||||
|
ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false);
|
||||||
|
auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer();
|
||||||
|
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB));
|
||||||
|
ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0);
|
||||||
|
ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0);
|
||||||
|
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer
|
||||||
|
// colour buffer, so the read lands four words per texel and the red one is compacted out
|
||||||
|
// here. The PACK scope is the tight default rather than the application's, so a row comes
|
||||||
|
// back packed at exactly `width * 4` words. One glGetError covers the whole loop: it
|
||||||
|
// accumulates, and a failure anywhere means the caller falls back rather than trusting a
|
||||||
|
// partial result.
|
||||||
|
const SizeT wordsPerRow = static_cast<SizeT>(width) * 4;
|
||||||
|
Vector<Uint32> staging(static_cast<SizeT>(bandRows) * wordsPerRow);
|
||||||
|
ScopedPixelPackBuffer packBuffer(0);
|
||||||
|
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||||
|
ClearGLErrors();
|
||||||
|
for (GLsizei y = 0; y < height; y += bandRows) {
|
||||||
|
const GLsizei rows = std::min(bandRows, height - y);
|
||||||
|
g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0,
|
||||||
|
0, 0, width, rows, 1);
|
||||||
|
g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data());
|
||||||
|
for (GLsizei row = 0; row < rows; ++row) {
|
||||||
|
const Uint32* srcRow = staging.data() + static_cast<SizeT>(row) * wordsPerRow;
|
||||||
|
Uint32* dstRow = outWords + static_cast<SizeT>(y + row) * static_cast<SizeT>(width);
|
||||||
|
for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast<SizeT>(x) * 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const GLenum error = g_GLESFuncs.glGetError();
|
||||||
|
if (error != GL_NO_ERROR) {
|
||||||
|
MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back",
|
||||||
|
MG_Util::ConvertGLEnumToString(backendTarget).c_str(),
|
||||||
|
MG_Util::ConvertGLEnumToString(error).c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
|
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
|
||||||
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
|
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
|
||||||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
||||||
@@ -7842,9 +8100,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
|
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
|
||||||
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
|
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level,
|
||||||
/*withStencil=*/format == GL_DEPTH_STENCIL);
|
/*withStencil=*/format == GL_DEPTH_STENCIL);
|
||||||
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) {
|
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
||||||
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY) {
|
||||||
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch).
|
// ES cannot attach 3D/array textures through glFramebufferTexture2D; layer 0 here, and
|
||||||
|
// the deeper slices one at a time in the per-layer loop below. A CUBE MAP ARRAY is in
|
||||||
|
// this list for the same reason its layer-faces are addressed as array layers:
|
||||||
|
// glFramebufferTexture2D has no target token for it, so the 2D attach it used to take
|
||||||
|
// left the scratch FBO incomplete and every read fell through to the (stale) CPU
|
||||||
|
// shadow - which is exactly the all-zero result the conformance suite saw.
|
||||||
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
|
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
|
||||||
} else {
|
} else {
|
||||||
ScratchFBOImpl::EnsureColorAttachment2D(
|
ScratchFBOImpl::EnsureColorAttachment2D(
|
||||||
@@ -7897,6 +8160,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level);
|
auto size = textureMipmapObject->GetMipmapTexelSize(MG_Util::ConvertGLEnumToTextureUploadTarget(target), level);
|
||||||
|
|
||||||
|
// GL_TEXTURE_1D_ARRAY keeps its LAYERS in the state-side height (that is what
|
||||||
|
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means), while the ES texture behind it is a
|
||||||
|
// 2D array of height 1 with the layers in depth - GetBackendUploadSize performs exactly
|
||||||
|
// that swap on the way in. Everything below addresses the ES image, so the same swap has
|
||||||
|
// to happen here: without it the readback asked layer 0 for a `layers`-row rectangle it
|
||||||
|
// does not have, and every layer but the first came back undefined (all zeroes on Adreno,
|
||||||
|
// KHR-GL4x.shader_image_load_store.basic-allTargets-*).
|
||||||
|
const Bool oneDimensionalArray = textureObject->GetTarget() == TextureTarget::Texture1DArray;
|
||||||
|
if (oneDimensionalArray) {
|
||||||
|
size = TextureImpl::GetBackendUploadSize(TextureTarget::Texture1DArray, size);
|
||||||
|
}
|
||||||
|
|
||||||
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
|
MGLOG_D("GetTexImage: mip level %d size = %dx%d", level, size.x(), size.y());
|
||||||
|
|
||||||
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs
|
// Prefer the client-format conversion for every convertible combination: the "native" ES pairs
|
||||||
@@ -7910,10 +8185,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
|
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
|
||||||
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
|
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
|
||||||
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
|
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
|
||||||
// them (GL 3.3 section 6.1.4).
|
// them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
|
||||||
const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D ||
|
// as a single two-dimensional image whose ROWS are the layers, so the layer stride is
|
||||||
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
// one packed row and the image parameters do not enter into it - even though the ES
|
||||||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
// texture underneath is an array and is read one layer at a time.
|
||||||
|
const Bool applyPackImageParams = !oneDimensionalArray &&
|
||||||
|
(backendAttachTarget == GL_TEXTURE_3D ||
|
||||||
|
backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
||||||
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY);
|
||||||
const GLsizei sliceCount = std::max(size.z(), 1);
|
const GLsizei sliceCount = std::max(size.z(), 1);
|
||||||
const Bool multiSlice = size.z() > 1;
|
const Bool multiSlice = size.z() > 1;
|
||||||
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
|
// glGetTexImage answers with the STORED texels, and for a packed format whose encoding
|
||||||
@@ -7922,17 +8201,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// value 8064, different words), and the conformance suite compares the words
|
// value 8064, different words), and the conformance suite compares the words
|
||||||
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
|
// ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT
|
||||||
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
|
// decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the
|
||||||
// shadow branch further down was unreachable. Serve the verbatim-word pairs from the
|
// shadow branch further down was unreachable. Every other format still prefers the
|
||||||
// shadow first and keep the GPU attempts as the fallback for a level the shadow never
|
// GPU, so a rendered-into texture is unaffected.
|
||||||
// received. Every other format still prefers the GPU, so a rendered-into texture is
|
const Bool rawPackedWordRead = MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||||
// unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative -
|
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||||
// and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the
|
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||||
// shadow for exactly this reason.
|
// ...and the GPU CAN answer with the stored words after all, for any 32-bit packed
|
||||||
|
// format and whoever wrote the level, by going through a scratch GL_R32UI image (see
|
||||||
|
// ReadPackedLevelWordsViaScratch). Preferred over both routes below because it is the
|
||||||
|
// only one that is right for a level glCopyImageSubData wrote: the shadow may never
|
||||||
|
// have seen that write, and re-encoding the attachment cannot reproduce an RGB9_E5
|
||||||
|
// shared exponent or an R11F_G11F_B10F NaN payload. A multisample image is excluded
|
||||||
|
// because copy-image requires matching sample counts.
|
||||||
|
if (rawPackedWordRead && textureObject->GetSamples() == 0) {
|
||||||
|
// Copy-image addresses a cube map as ONE image with the face on z, where
|
||||||
|
// glGetTexImage names the face in its target.
|
||||||
|
const auto readUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||||
|
const GLint copyBaseSlice =
|
||||||
|
(readUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||||
|
readUploadTarget <= TextureUploadTarget::CubeMapNegativeZ)
|
||||||
|
? static_cast<GLint>(readUploadTarget) -
|
||||||
|
static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX)
|
||||||
|
: 0;
|
||||||
|
const GLenum copyTarget =
|
||||||
|
TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
||||||
|
const SizeT sliceWords = static_cast<SizeT>(size.x()) * static_cast<SizeT>(size.y());
|
||||||
|
Vector<Uint32> words(sliceWords * static_cast<SizeT>(sliceCount));
|
||||||
|
Bool allSlicesRead = true;
|
||||||
|
for (GLsizei slice = 0; slice < sliceCount && allSlicesRead; ++slice) {
|
||||||
|
allSlicesRead = ReadPackedLevelWordsViaScratch(backendTexId, copyTarget, level,
|
||||||
|
copyBaseSlice + slice, size.x(), size.y(),
|
||||||
|
words.data() + sliceWords * static_cast<SizeT>(slice));
|
||||||
|
}
|
||||||
|
if (allSlicesRead &&
|
||||||
|
ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(words.data()), size.x(),
|
||||||
|
size.y(), sliceCount, type, pixels,
|
||||||
|
applyPackImageParams)) {
|
||||||
|
MGLOG_D("GetTexImage: finished %d slice(s) via the bit-exact GL_R32UI word readback", sliceCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The last resort for the one format the attachment route can never answer for: the
|
||||||
|
// shadow is only right while nothing but a glTexImage has written the level, which is
|
||||||
|
// why CopyImageSubData mirrors itself into it where it can.
|
||||||
const Bool verbatimPackedShadowRead =
|
const Bool verbatimPackedShadowRead =
|
||||||
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
|
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
|
||||||
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
rawPackedWordRead;
|
||||||
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
|
||||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
|
||||||
if (verbatimPackedShadowRead &&
|
if (verbatimPackedShadowRead &&
|
||||||
GetTexImageViaShadowConversion(textureMipmapObject,
|
GetTexImageViaShadowConversion(textureMipmapObject,
|
||||||
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
||||||
@@ -7947,7 +8261,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Attach the layers one at a time instead and read each off the GPU, keeping the shadow
|
// Attach the layers one at a time instead and read each off the GPU, keeping the shadow
|
||||||
// for the formats the FBO cannot represent at all.
|
// for the formats the FBO cannot represent at all.
|
||||||
if (multiSlice && tempFBOComplete &&
|
if (multiSlice && tempFBOComplete &&
|
||||||
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY)) {
|
(backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY ||
|
||||||
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY)) {
|
||||||
// Each slice is packed as its own 2D image, so the per-slice call must not apply
|
// Each slice is packed as its own 2D image, so the per-slice call must not apply
|
||||||
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
|
// GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination
|
||||||
// over them, using the same layout StoreWideRowsToClient computes.
|
// over them, using the same layout StoreWideRowsToClient computes.
|
||||||
@@ -9125,6 +9440,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
XfbImpl::OnBackendContextDestroyed();
|
XfbImpl::OnBackendContextDestroyed();
|
||||||
MultiDrawImpl::OnBackendContextDestroyed();
|
MultiDrawImpl::OnBackendContextDestroyed();
|
||||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||||
|
ReleasePackedWordScratchTexture();
|
||||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||||
PixelStoreImpl::InvalidatePackStateCache();
|
PixelStoreImpl::InvalidatePackStateCache();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "BackendObject_DirectGLES.h"
|
#include "BackendObject_DirectGLES.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
|
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
@@ -45,6 +47,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
|
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
|
||||||
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
|
constexpr const char* ZERO_BASED_INSTANCE_ID_NAME = "mg_ZeroBasedInstanceID";
|
||||||
|
|
||||||
|
// ES has no atomic-counter buffers: glslang lowers every atomic_uint onto a synthesized
|
||||||
|
// storage block, so one GL counter BUFFER costs one of the driver's shader-storage binding
|
||||||
|
// points. Those slots are taken from the TOP of the range downwards - below the one
|
||||||
|
// mg_IndirectParams already reserves - so an application binding its own SSBOs from 0 upwards
|
||||||
|
// never meets them, and the slot for GL binding N is `this - N` in every stage of the
|
||||||
|
// program without any shared state. Negative when the driver has no room left at all.
|
||||||
|
static Int AtomicCounterEsslBindingTop() {
|
||||||
|
return g_GLESCapabilities.MaxShaderStorageBufferBindings - 2;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool IsAngleLlvmpipeRenderer() {
|
static Bool IsAngleLlvmpipeRenderer() {
|
||||||
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
|
return g_GLESCapabilities.IsAngleLlvmpipeRenderer;
|
||||||
}
|
}
|
||||||
@@ -81,6 +93,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL
|
||||||
|
// advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl
|
||||||
|
// AND is part of the L2 translation-memo key, so the transpile and the key must read
|
||||||
|
// the same constant - hence one definition rather than two locals.
|
||||||
|
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is
|
||||||
|
// backend code and must not reach into the GL frontend. 4 is that translation unit's
|
||||||
|
// kFrontendMaxSamples, which is the source of truth - keep the two in step.
|
||||||
|
constexpr Int kFrontendMaxSamples = 4;
|
||||||
|
|
||||||
static Uint ResolveBackendEsslVersion() {
|
static Uint ResolveBackendEsslVersion() {
|
||||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||||
if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) {
|
if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) {
|
||||||
@@ -1758,14 +1779,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||||
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
|
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
|
||||||
|
|
||||||
// Defence in depth. The frontend already declines glVertexAttribLFormat on this
|
// This is where a 64-bit array actually stops. glVertexAttribLFormat is a legal call
|
||||||
// backend (SupportsFloat64VertexAttributes is false - ES has no GL_DOUBLE vertex
|
// in a GL 4.3 context and the frontend RECORDS its format (the state queries have to
|
||||||
// format and ESSL has no fp64 type), so IsLong should never arrive here; if it ever
|
// answer), so IsLong does arrive here - what this backend cannot do is FEED it:
|
||||||
// did, passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on
|
// SupportsFloat64VertexAttributes is false because ES has no GL_DOUBLE vertex format
|
||||||
// the real driver. Disabling rather than merely skipping matters: becoming long bumps
|
// and ESSL has no fp64 type, and passing GL_DOUBLE to glVertexAttribPointer would
|
||||||
// FormatVersion, not SwitchVersion, so the enable/disable block above will not run
|
// only raise GL_INVALID_ENUM on the real driver. Disabling rather than merely
|
||||||
// again and an already-enabled array would stay enabled with no pointer and no
|
// skipping matters: becoming long bumps FormatVersion, not SwitchVersion, so the
|
||||||
// ARRAY_BUFFER binding - which ES 3.1+ makes an INVALID_OPERATION at draw.
|
// enable/disable block above will not run again and an already-enabled array would
|
||||||
|
// stay enabled with no pointer and no ARRAY_BUFFER binding - which ES 3.1+ makes an
|
||||||
|
// INVALID_OPERATION at draw.
|
||||||
//
|
//
|
||||||
// IsLong is not the only way a 64-bit array gets here: glVertexAttribFormat
|
// IsLong is not the only way a 64-bit array gets here: glVertexAttribFormat
|
||||||
// with GL_DOUBLE asks for doubles in memory CONVERTED to float, so it is not
|
// with GL_DOUBLE asks for doubles in memory CONVERTED to float, so it is not
|
||||||
@@ -4703,6 +4726,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return signature + entry;
|
return signature + entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pipeline position of a shader stage. Names the PRODUCER of an inter-stage
|
||||||
|
// interface block: a block one stage consumes was written by the stage before it.
|
||||||
|
// ShaderStage is declared in pipeline order, so the enum value IS the position;
|
||||||
|
// compute has no inter-stage interface at all and is reported as -1.
|
||||||
|
Int InterStagePipelineIndex(ShaderStage stage) {
|
||||||
|
switch (stage) {
|
||||||
|
case ShaderStage::Vertex:
|
||||||
|
case ShaderStage::TessControl:
|
||||||
|
case ShaderStage::TessEval:
|
||||||
|
case ShaderStage::Geometry:
|
||||||
|
case ShaderStage::Fragment:
|
||||||
|
return static_cast<Int>(stage);
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether a stage can declare interface blocks in BOTH directions at once, i.e.
|
||||||
|
// whether one block name can name two different blocks inside it. A vertex INPUT
|
||||||
|
// and a fragment OUTPUT cannot be blocks and compute has neither, so only these
|
||||||
|
// three can. This is what keeps the module probe off every program without
|
||||||
|
// tessellation or geometry - which is every program Minecraft and its shader packs
|
||||||
|
// build.
|
||||||
|
Bool CanDeclareBlocksInBothDirections(ShaderStage stage) {
|
||||||
|
return stage == ShaderStage::TessControl || stage == ShaderStage::TessEval ||
|
||||||
|
stage == ShaderStage::Geometry;
|
||||||
|
}
|
||||||
|
|
||||||
// Reflection names an array uniform after its first element ("g_image[0]") at every
|
// Reflection names an array uniform after its first element ("g_image[0]") at every
|
||||||
// location it spans; SPIR-V names the variable once, without the subscript. This is
|
// location it spans; SPIR-V names the variable once, without the subscript. This is
|
||||||
// the name both sides agree on.
|
// the name both sides agree on.
|
||||||
@@ -4745,18 +4796,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||||
ImageFormatBakeInputs inputs;
|
ImageFormatBakeInputs inputs;
|
||||||
|
// A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it
|
||||||
|
// with. There is no legal ESSL for such a shader at all, so the stage will not
|
||||||
|
// compile and the program is lost - a failure that used to leave nothing behind but
|
||||||
|
// a draw that rendered nothing. Recorded and reported ONCE per program build rather
|
||||||
|
// than per uniform: an image array reaches this decision once per element.
|
||||||
|
String unspellableUniform;
|
||||||
|
String unspellableFormat;
|
||||||
|
Uint unspellableCount = 0;
|
||||||
|
const auto recordUnspellableFormat = [&](const String& uniformName, String formatSpelling) {
|
||||||
|
if (unspellableCount == 0) {
|
||||||
|
unspellableUniform = uniformName;
|
||||||
|
unspellableFormat = Move(formatSpelling);
|
||||||
|
}
|
||||||
|
++unspellableCount;
|
||||||
|
};
|
||||||
|
|
||||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||||
if (name.empty()) continue;
|
if (name.empty()) continue;
|
||||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||||
const glslang::TType* type = stateProgramObject.GetUniformTType(loc);
|
const auto& type = stateProgramObject.GetUniformTypeFacts(loc);
|
||||||
if (type == nullptr) continue;
|
if (type.hasFormat) {
|
||||||
if (type->getQualifier().hasFormat()) {
|
|
||||||
// Declared, and therefore left exactly as written - but a non-core spelling
|
// Declared, and therefore left exactly as written - but a non-core spelling
|
||||||
// still needs the extension directive to survive the ES compiler.
|
// still needs the extension directive to survive the ES compiler.
|
||||||
if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) {
|
if (!IsCoreEsslLayoutFormat(static_cast<glslang::TLayoutFormat>(type.layoutFormat))) {
|
||||||
inputs.needsExtendedImageFormats = true;
|
inputs.needsExtendedImageFormats = true;
|
||||||
|
if (!g_GLESCapabilities.SupportsExtendedImageFormats) {
|
||||||
|
// From the OWNED TypeFacts, not from a live TType: the reflection
|
||||||
|
// snapshot already carries the declared layout format, and there is
|
||||||
|
// no glslang object to ask on a translation-cache L1 hit.
|
||||||
|
recordUnspellableFormat(
|
||||||
|
name, glslang::TQualifier::getLayoutFormatString(
|
||||||
|
static_cast<glslang::TLayoutFormat>(type.layoutFormat)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4782,6 +4856,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
|
MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES "
|
||||||
"core cannot spell and this driver has no GL_NV_image_formats for.",
|
"core cannot spell and this driver has no GL_NV_image_formats for.",
|
||||||
name.c_str(), unit, boundFormat);
|
name.c_str(), unit, boundFormat);
|
||||||
|
recordUnspellableFormat(
|
||||||
|
name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
inputs.needsExtendedImageFormats = true;
|
inputs.needsExtendedImageFormats = true;
|
||||||
@@ -4824,6 +4900,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (const auto& name : textCompleted) {
|
for (const auto& name : textCompleted) {
|
||||||
inputs.glFormatByUniformName.erase(name);
|
inputs.glFormatByUniformName.erase(name);
|
||||||
}
|
}
|
||||||
|
// Unlatched MGLOG_E, like the transpile- and link-failure diagnostics in SyncToBackend:
|
||||||
|
// one line per failing program build, and naming the uniform and the format is the
|
||||||
|
// whole diagnostic value. Left as a log rather than a link failure on purpose - the
|
||||||
|
// frontend has already reported LINK_STATUS = true and GL cannot retract it, and the
|
||||||
|
// program stays queryable exactly as the "linked but not drawable" exit leaves it.
|
||||||
|
if (unspellableCount != 0) {
|
||||||
|
MGLOG_E("Image format '%s' on uniform '%s' has no GLSL ES spelling and this driver does not expose "
|
||||||
|
"GL_NV_image_formats%s; the stage using it cannot compile and the program will draw "
|
||||||
|
"nothing.",
|
||||||
|
unspellableFormat.empty() ? "(none)" : unspellableFormat.c_str(), unspellableUniform.c_str(),
|
||||||
|
unspellableCount > 1 ? " (and it is not the only image uniform affected)" : "");
|
||||||
|
}
|
||||||
return inputs;
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4841,6 +4929,338 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
|
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) =====
|
||||||
|
//
|
||||||
|
// One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the
|
||||||
|
// DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the
|
||||||
|
// boundary the L2 memo keys on is a function signature rather than a comment: every
|
||||||
|
// input this reads is either an argument below or a process-global capability bit,
|
||||||
|
// and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add
|
||||||
|
// it to BuildEsslTranslationKey too - an under-specified key here is a silently
|
||||||
|
// miscompiled shader.
|
||||||
|
//
|
||||||
|
// Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray,
|
||||||
|
// MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples,
|
||||||
|
// SupportsNoperspectiveInterpolation, GLESVersion} (the last via
|
||||||
|
// ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
|
||||||
|
//
|
||||||
|
// Deliberately NOT in here, and therefore NOT in the key: the text-level passes that
|
||||||
|
// follow in SyncToBackend. They are cheap string work and they read a long tail of
|
||||||
|
// live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject
|
||||||
|
// reflection, the norm-clamp masks and the fragColor broadcast count are live
|
||||||
|
// globals, the buffer-texture tier retargets an #extension line) whose inclusion
|
||||||
|
// would make the key both enormous and fragile for no measurable saving.
|
||||||
|
//
|
||||||
|
// Returns false when SPIRV-Cross refused the module; `outError` then holds its
|
||||||
|
// message and nothing is memoized.
|
||||||
|
Bool BackendProgramObjectImpl::TranspileSpirvToEssl(
|
||||||
|
const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
|
||||||
|
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
|
||||||
|
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
const Int atomicCounterEsslBindingTop, const Bool enableSpirvValidation, String& outSource,
|
||||||
|
std::set<String>& outFlattenedXfbBlockNames, Vector<Int>& outAtomicCounterGlBindings,
|
||||||
|
String& outError) const {
|
||||||
|
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
|
||||||
|
// plain globals (mg_*) before handing the module to SPIRV-Cross.
|
||||||
|
Vector<unsigned int> loweredSpirv;
|
||||||
|
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
||||||
|
if (glShaderType == GL_VERTEX_SHADER &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
||||||
|
!loweredSpirv.empty()) {
|
||||||
|
effectiveSpirv = &loweredSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
||||||
|
// there IS an extension that provides it - so this runs only when the driver does
|
||||||
|
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
||||||
|
// gets the `#extension` request added to the decompiled source below instead.
|
||||||
|
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
|
||||||
|
// viewport 0), which is the degradation ViewportArrayScenario already documents
|
||||||
|
// for this backend; NOT demoting it costs the whole program, because the stage
|
||||||
|
// fails to compile and every draw made with it silently renders nothing.
|
||||||
|
// Gated on the module actually declaring the output, so no other stage pays an
|
||||||
|
// optimizer round trip for it.
|
||||||
|
// One parse of the module answers every armed pass gate below. The per-gate
|
||||||
|
// Declares* probes each cost a BuildModule per stage, and on a driver where both
|
||||||
|
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
|
||||||
|
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
|
||||||
|
// Probing the pre-lowering module is sound for both gates: demoting
|
||||||
|
// gl_ViewportIndex neither adds nor removes multisampled image types.
|
||||||
|
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
||||||
|
// this is backend code and must not reach into the GL frontend. 4 is that
|
||||||
|
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
||||||
|
// keep the two in step.
|
||||||
|
const Int advertisedMaxSamples =
|
||||||
|
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||||
|
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
||||||
|
const Bool sampleClampArmed =
|
||||||
|
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||||
|
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||||
|
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
||||||
|
if (viewportLoweringArmed || sampleClampArmed) {
|
||||||
|
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
||||||
|
*effectiveSpirv);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<unsigned int> loweredViewportSpirv;
|
||||||
|
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
|
||||||
|
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
||||||
|
!loweredViewportSpirv.empty()) {
|
||||||
|
effectiveSpirv = &loweredViewportSpirv;
|
||||||
|
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
||||||
|
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
||||||
|
"global; every invocation renders into viewport 0.",
|
||||||
|
m_backendProgramId,
|
||||||
|
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
||||||
|
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
||||||
|
// reports - but the realised allocation cannot be, and
|
||||||
|
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
||||||
|
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
||||||
|
// written against the advertised ceiling then fetches a sample that storage does
|
||||||
|
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
||||||
|
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
||||||
|
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
||||||
|
// inside the allocation. Gated on some category actually being squeezed AND the
|
||||||
|
// module actually declaring a multisampled image, so no other stage pays an
|
||||||
|
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
||||||
|
// allocates the sample count it was asked for, so its modules are already right.
|
||||||
|
Vector<unsigned int> clampedSampleSpirv;
|
||||||
|
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||||
|
*effectiveSpirv, clampedSampleSpirv,
|
||||||
|
g_GLESCapabilities.MaxColorTextureSamples,
|
||||||
|
g_GLESCapabilities.MaxIntegerSamples,
|
||||||
|
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
||||||
|
enableSpirvValidation) &&
|
||||||
|
!clampedSampleSpirv.empty()) {
|
||||||
|
effectiveSpirv = &clampedSampleSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
||||||
|
// rather than emulating them, so this has to happen before it sees the binary.
|
||||||
|
Vector<unsigned int> splitArrayInputSpirv;
|
||||||
|
if (glShaderType == GL_VERTEX_SHADER &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
||||||
|
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
||||||
|
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
||||||
|
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
||||||
|
// re-serialised copy either way, and adopting that copy for every vertex
|
||||||
|
// shader would put every one of them through a round trip they do not need
|
||||||
|
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
|
||||||
|
// first time this gate was missing.
|
||||||
|
effectiveSpirv = &splitArrayInputSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adopt the rewritten module only when THIS stage actually had one of the
|
||||||
|
// blocks - the optimizer hands back a re-serialised copy either way, and taking
|
||||||
|
// that copy for a module it did not rewrite is not free (it cost the
|
||||||
|
// create-indirect retrace 0.15 SSIM when the array-input split first missed
|
||||||
|
// this gate). The report has to be per stage, not cumulative: a fragment shader
|
||||||
|
// consuming the same block reports a name the vertex stage already reported,
|
||||||
|
// and its own rewrite must still be taken or the two stages stop matching.
|
||||||
|
Vector<unsigned int> flattenedXfbSpirv;
|
||||||
|
if (!xfbCaptureBlockNames.empty()) {
|
||||||
|
// Reported into a local first, and published only if the module is really
|
||||||
|
// adopted. The caller unions the published set unconditionally (so that a
|
||||||
|
// cache HIT contributes its names too), so publishing a name for a rewrite
|
||||||
|
// that was declined would rename a capture the emitted ESSL never renamed.
|
||||||
|
std::set<String> flattenedNames;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
||||||
|
*effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv,
|
||||||
|
enableSpirvValidation) &&
|
||||||
|
!flattenedXfbSpirv.empty() && !flattenedNames.empty()) {
|
||||||
|
effectiveSpirv = &flattenedXfbSpirv;
|
||||||
|
outFlattenedXfbBlockNames = Move(flattenedNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The producer-keyed interface-block rename, planned program-wide by the caller and
|
||||||
|
// applied to this stage: the blocks it CONSUMES are spelled after the previous stage
|
||||||
|
// present in the program and the ones it PRODUCES after itself, so a tessellation
|
||||||
|
// evaluation stage's two TCSOutputBlocks stop being one name and every other stage
|
||||||
|
// still agrees with it. See UniquifyIoBlockNamesPass for why Mali needs it.
|
||||||
|
//
|
||||||
|
// INSIDE THE MEMOIZED SEGMENT, and at exactly the position it was written in - after
|
||||||
|
// the XFB flatten, before the UBO precision strip. Both halves of that matter:
|
||||||
|
// * INSIDE, because it rewrites the MODULE and the emitted ESSL carries the result.
|
||||||
|
// Left outside, a second program sharing this stage's key would be served ESSL
|
||||||
|
// with the blocks un-renamed and the repair would silently stop working - the
|
||||||
|
// same trap SetAtomicCounterBlockBindings sets one screen down.
|
||||||
|
// * AT THIS POSITION, because moving a SPIR-V pass in a chain is a behavioural
|
||||||
|
// change, and this one arrived device-verified on Mali. Hoisting it above the
|
||||||
|
// cache probe instead would have needed no key material at all (the rename would
|
||||||
|
// already be in the module bytes the key hashes) and was rejected for that
|
||||||
|
// reason: it reorders the chain, and it would re-serialise the module on every
|
||||||
|
// build including the ones the memo is there to make free.
|
||||||
|
// Its two rename maps are therefore KEY MATERIAL - see the caller.
|
||||||
|
Vector<unsigned int> uniquifiedIoBlockSpirv;
|
||||||
|
if (!inputBlockRenames.empty() || !outputBlockRenames.empty()) {
|
||||||
|
std::set<String> stageRenamedIoBlockNames;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::UniquifyIoBlockNamesForEssl(
|
||||||
|
*effectiveSpirv, inputBlockRenames, outputBlockRenames,
|
||||||
|
stageRenamedIoBlockNames, uniquifiedIoBlockSpirv, enableSpirvValidation) &&
|
||||||
|
!uniquifiedIoBlockSpirv.empty() && !stageRenamedIoBlockNames.empty()) {
|
||||||
|
effectiveSpirv = &uniquifiedIoBlockSpirv;
|
||||||
|
MGLOG_D("Program %u stage %s: %zu inter-stage interface block(s) renamed per "
|
||||||
|
"producing stage, because some stage of this program declares the same "
|
||||||
|
"block name in both directions and the ES driver may alias the two.",
|
||||||
|
m_backendProgramId, MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||||
|
stageRenamedIoBlockNames.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||||
|
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||||
|
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||||
|
// ForceSupporterOutput swaps the fragment header to highp, that member reads
|
||||||
|
// back as highp and the ES driver refuses to link ("definitions of uniform
|
||||||
|
// block ... do not match"). Strip the hint from block structs so both stages
|
||||||
|
// declare the member highp; nothing else about emission changes.
|
||||||
|
Vector<unsigned int> uboPrecisionSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||||
|
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
||||||
|
!uboPrecisionSpirv.empty()) {
|
||||||
|
effectiveSpirv = &uboPrecisionSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
||||||
|
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
||||||
|
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
||||||
|
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
||||||
|
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
||||||
|
// the decoration - exact, extension-free. Devices that have the extension keep the
|
||||||
|
// decoration and let the hardware do it natively.
|
||||||
|
Vector<unsigned int> noperspectiveSpirv;
|
||||||
|
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||||
|
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
||||||
|
!noperspectiveSpirv.empty()) {
|
||||||
|
effectiveSpirv = &noperspectiveSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||||
|
// than approximating one. The shared pass turns the type into the 2D one and
|
||||||
|
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||||
|
// size, which is the whole of the difference between the two.
|
||||||
|
Vector<unsigned int> rectLoweredSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
||||||
|
!rectLoweredSpirv.empty()) {
|
||||||
|
effectiveSpirv = &rectLoweredSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
|
||||||
|
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
|
||||||
|
// as 2D for images without ever asking whether the type is arrayed, so a
|
||||||
|
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
|
||||||
|
// two-component constructor, which every driver rejects, taking the whole
|
||||||
|
// program with it. The pass does the conversion properly - type to 2D array,
|
||||||
|
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
||||||
|
Vector<unsigned int> arrayImageSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
||||||
|
arrayImageSpirv, enableSpirvValidation) &&
|
||||||
|
!arrayImageSpirv.empty()) {
|
||||||
|
effectiveSpirv = &arrayImageSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
||||||
|
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
||||||
|
// format"), which loses the whole program. Give each such image the format the
|
||||||
|
// application bound to its unit - the one GL's format-class rules make correct -
|
||||||
|
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
|
||||||
|
// also rewrites image types, so this one is looking at the final shapes.
|
||||||
|
//
|
||||||
|
// Gated on the module actually declaring one: the map is empty for every program
|
||||||
|
// whose images all declare formats, and the cheap probe keeps a program that has
|
||||||
|
// an unbound format-less image from paying an optimizer round trip per stage.
|
||||||
|
Vector<unsigned int> imageFormatSpirv;
|
||||||
|
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
||||||
|
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
||||||
|
enableSpirvValidation) &&
|
||||||
|
!imageFormatSpirv.empty()) {
|
||||||
|
effectiveSpirv = &imageFormatSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLSL ES demands a constant integral expression to index a fragment output
|
||||||
|
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
|
||||||
|
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
|
||||||
|
// outright ("array indexes for fragment outputs must be constant integral
|
||||||
|
// expressions"), linking no program and silently no-oping every draw that
|
||||||
|
// uses it. Mesa accepts it, ANGLE does not - which is the whole of the
|
||||||
|
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
|
||||||
|
// on the ESSL path only: the same module is legal for DirectVulkan.
|
||||||
|
Vector<unsigned int> outputIndexSpirv;
|
||||||
|
if (glShaderType == GL_FRAGMENT_SHADER &&
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
||||||
|
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
||||||
|
!outputIndexSpirv.empty()) {
|
||||||
|
effectiveSpirv = &outputIndexSpirv;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||||
|
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
|
||||||
|
spvc_compiler_options options;
|
||||||
|
spvcSession.CreateOptions(&options);
|
||||||
|
|
||||||
|
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION,
|
||||||
|
ResolveBackendEsslVersion());
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||||
|
|
||||||
|
spvcSession.SetOptions(options);
|
||||||
|
|
||||||
|
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
|
||||||
|
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
|
||||||
|
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
|
||||||
|
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
|
||||||
|
// then deliberately preserves the qualifier for `buffer` declarations.
|
||||||
|
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
|
||||||
|
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic counters, same mechanism for the same reason. glslang already turned
|
||||||
|
// every atomic_uint into a member of gl_AtomicCounterBlock_<N> and let the IO
|
||||||
|
// mapper pick that block's binding, which has no relation to the GL binding point
|
||||||
|
// N the application bound its counter buffer to - and can alias an SSBO the
|
||||||
|
// application binds itself. Move each block to its reserved slot and record N, so
|
||||||
|
// the draw path knows which GL_ATOMIC_COUNTER_BUFFER points to re-issue as
|
||||||
|
// storage-buffer bindings.
|
||||||
|
//
|
||||||
|
// BOTH HALVES ARE MEMO STATE. `atomicCounterEsslBindingTop` decides the binding
|
||||||
|
// this prints into the ESSL, so it is in the L2 key; `outAtomicCounterGlBindings`
|
||||||
|
// is an OUTPUT this stage produces and the draw path consumes, so it is in the L2
|
||||||
|
// payload. A hit that replayed only the text would leave the bindings empty and
|
||||||
|
// every counter buffer unbound - the same class of silent loss the flattened XFB
|
||||||
|
// block names would have been.
|
||||||
|
spvcSession.SetAtomicCounterBlockBindings(atomicCounterEsslBindingTop,
|
||||||
|
outAtomicCounterGlBindings);
|
||||||
|
|
||||||
|
const char* result = nullptr;
|
||||||
|
spvcSession.Compile(&result);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
// The caller owns the diagnostic: it is the one that knows the
|
||||||
|
// frontend program id, and a failed transpile must NOT be memoized -
|
||||||
|
// the message names the stage and is worth re-emitting every time.
|
||||||
|
const char* lastError = spvcSession.GetLastErrorString();
|
||||||
|
outError = lastError ? lastError : "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outSource = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void BackendProgramObjectImpl::SyncToBackend(
|
void BackendProgramObjectImpl::SyncToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -4881,6 +5301,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// this build current - the draw path compares the signature and rebuilds on a change.
|
// this build current - the draw path compares the signature and rebuilds on a change.
|
||||||
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
|
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
|
||||||
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
|
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
|
||||||
|
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
|
||||||
|
// The top is snapshotted here so every stage of this program - and the draw path
|
||||||
|
// reading it afterwards - resolves the same slot for the same GL binding.
|
||||||
|
m_atomicCounterGlBindings.clear();
|
||||||
|
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
|
||||||
// The same shape again for image FORMATS: what a format-less image declaration
|
// 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
|
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||||
@@ -4947,6 +5372,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
std::set<String> flattenedXfbBlockNames;
|
std::set<String> flattenedXfbBlockNames;
|
||||||
|
|
||||||
|
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface
|
||||||
|
// blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at
|
||||||
|
// the same time - which the tessellation evaluation stage of both interface-block
|
||||||
|
// tests in KHR-GL42/43.shading_language_420pack does ("in TCSOutputBlock ... out
|
||||||
|
// TCSOutputBlock"). SPIRV-Cross keeps the same split (block_input_names vs
|
||||||
|
// block_output_names) and re-emits BOTH under the name FOO, so the generated ESSL
|
||||||
|
// declares two different blocks called FOO in one shader. Adreno's ES compiler
|
||||||
|
// keeps them apart; Mali's does not - the stage compiles, the program links, and
|
||||||
|
// the output block's payload never reaches the next stage. All 22 of that group's
|
||||||
|
// Mali failures are exactly the two tests that write this shape, and every one of
|
||||||
|
// them passes on Adreno and on DirectVulkan.
|
||||||
|
//
|
||||||
|
// The repair is a rename keyed on the PRODUCING stage, planned here and applied
|
||||||
|
// per stage below so a producer and its consumer keep naming the same block.
|
||||||
|
// Gated twice over, because a re-serialised module is not free (it cost the
|
||||||
|
// create-indirect retrace 0.15 SSIM the first time the array-input split missed
|
||||||
|
// its gate): only a tessellation or geometry stage can declare blocks in both
|
||||||
|
// directions at all, and even then the probe has to FIND a collision before any
|
||||||
|
// stage is rewritten.
|
||||||
|
std::set<String> collidingIoBlockNames;
|
||||||
|
std::set<String> declaredIoBlockNames;
|
||||||
|
Vector<Int> stagePipelineIndices(attachedShaders.size(), -1);
|
||||||
|
Bool anyStageCanDeclareBlocksInBothDirections = false;
|
||||||
|
for (SizeT index = 0; index < attachedShaders.size(); ++index) {
|
||||||
|
const ShaderStage stage = attachedShaders[index]->GetShaderStage();
|
||||||
|
stagePipelineIndices[index] = InterStagePipelineIndex(stage);
|
||||||
|
if (CanDeclareBlocksInBothDirections(stage)) anyStageCanDeclareBlocksInBothDirections = true;
|
||||||
|
}
|
||||||
|
if (anyStageCanDeclareBlocksInBothDirections) {
|
||||||
|
for (SizeT index = 0; index < attachedShaders.size() && index < shaderSpirvs.size(); ++index) {
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::ProbeIoBlockNamesForEssl(
|
||||||
|
shaderSpirvs[index], collidingIoBlockNames, declaredIoBlockNames);
|
||||||
|
}
|
||||||
|
// A block a capture request names is resolved BY NAME at
|
||||||
|
// glTransformFeedbackVaryings time - and flattened away entirely by the pass
|
||||||
|
// below - so renaming one would ask the driver for a block the request does
|
||||||
|
// not spell.
|
||||||
|
for (const auto& xfbCaptureBlockName : xfbCaptureBlockNames) {
|
||||||
|
collidingIoBlockNames.erase(xfbCaptureBlockName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The one spelling every stage of THIS program agrees on for `blockName` as written
|
||||||
|
// by pipeline stage `producerPipelineIndex`. "__" is reserved in GLSL, so a name
|
||||||
|
// already ending in '_' does not get another one, and the digit-suffix loop steps
|
||||||
|
// off any name the program already spells.
|
||||||
|
const auto uniqueIoBlockName = [&declaredIoBlockNames](const String& blockName,
|
||||||
|
Int producerPipelineIndex) {
|
||||||
|
const char* separator = (!blockName.empty() && blockName.back() == '_') ? "" : "_";
|
||||||
|
String candidate = blockName + separator + "mgio" + std::to_string(producerPipelineIndex);
|
||||||
|
while (declaredIoBlockNames.find(candidate) != declaredIoBlockNames.end()) {
|
||||||
|
candidate += "0";
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
};
|
||||||
|
|
||||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||||
auto& shader = attachedShaders[index];
|
auto& shader = attachedShaders[index];
|
||||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||||
@@ -4979,254 +5459,144 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
|
// ---- L2 of the shader translation memo -------------------------------
|
||||||
// plain globals (mg_*) before handing the module to SPIRV-Cross.
|
// The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage,
|
||||||
Vector<unsigned int> loweredSpirv;
|
// memoized on the module bytes and on every capability bit and per-program
|
||||||
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
// input that steers them. See TranslationCache.h for the key inventory and
|
||||||
if (glShaderType == GL_VERTEX_SHADER &&
|
// for why the text-level passes below stay outside the boundary.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
|
||||||
!loweredSpirv.empty()) {
|
esslKeyInputs.spirv = &spirvCode;
|
||||||
effectiveSpirv = &loweredSpirv;
|
esslKeyInputs.shaderType = glShaderType;
|
||||||
}
|
esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray;
|
||||||
|
esslKeyInputs.supportsNoperspectiveInterpolation =
|
||||||
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
|
||||||
// there IS an extension that provides it - so this runs only when the driver does
|
esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples;
|
||||||
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples;
|
||||||
// gets the `#extension` request added to the decompiled source below instead.
|
esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples;
|
||||||
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
|
esslKeyInputs.advertisedMaxSamples =
|
||||||
// viewport 0), which is the degradation ViewportArrayScenario already documents
|
|
||||||
// for this backend; NOT demoting it costs the whole program, because the stage
|
|
||||||
// fails to compile and every draw made with it silently renders nothing.
|
|
||||||
// Gated on the module actually declaring the output, so no other stage pays an
|
|
||||||
// optimizer round trip for it.
|
|
||||||
// One parse of the module answers every armed pass gate below. The per-gate
|
|
||||||
// Declares* probes each cost a BuildModule per stage, and on a driver where both
|
|
||||||
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
|
|
||||||
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
|
|
||||||
// Probing the pre-lowering module is sound for both gates: demoting
|
|
||||||
// gl_ViewportIndex neither adds nor removes multisampled image types.
|
|
||||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
|
||||||
// this is backend code and must not reach into the GL frontend. 4 is that
|
|
||||||
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
|
||||||
// keep the two in step.
|
|
||||||
constexpr Int kFrontendMaxSamples = 4;
|
|
||||||
const Int advertisedMaxSamples =
|
|
||||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||||
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames;
|
||||||
const Bool sampleClampArmed =
|
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
|
||||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
|
||||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
|
||||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
esslKeyInputs.atomicCounterEsslBindingTop = m_atomicCounterEsslBindingTop;
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
|
||||||
if (viewportLoweringArmed || sampleClampArmed) {
|
|
||||||
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
|
||||||
*effectiveSpirv);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<unsigned int> loweredViewportSpirv;
|
// THIS STAGE's share of the program-wide interface-block rename plan built above
|
||||||
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
|
// the loop. Resolved here, outside the memoized segment, because it is planning
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
|
// and not translation - exactly like the image-format bake map - and because that
|
||||||
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
// makes the two maps a plain function argument the L2 key can carry.
|
||||||
!loweredViewportSpirv.empty()) {
|
|
||||||
effectiveSpirv = &loweredViewportSpirv;
|
|
||||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
|
||||||
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
|
||||||
"global; every invocation renders into viewport 0.",
|
|
||||||
m_backendProgramId,
|
|
||||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
|
||||||
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
|
||||||
// reports - but the realised allocation cannot be, and
|
|
||||||
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
|
||||||
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
|
||||||
// written against the advertised ceiling then fetches a sample that storage does
|
|
||||||
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
|
||||||
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
|
||||||
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
|
||||||
// inside the allocation. Gated on some category actually being squeezed AND the
|
|
||||||
// module actually declaring a multisampled image, so no other stage pays an
|
|
||||||
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
|
||||||
// allocates the sample count it was asked for, so its modules are already right.
|
|
||||||
Vector<unsigned int> clampedSampleSpirv;
|
|
||||||
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
*effectiveSpirv, clampedSampleSpirv,
|
|
||||||
g_GLESCapabilities.MaxColorTextureSamples,
|
|
||||||
g_GLESCapabilities.MaxIntegerSamples,
|
|
||||||
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
|
||||||
enableSpirvValidation) &&
|
|
||||||
!clampedSampleSpirv.empty()) {
|
|
||||||
effectiveSpirv = &clampedSampleSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
|
||||||
// rather than emulating them, so this has to happen before it sees the binary.
|
|
||||||
Vector<unsigned int> splitArrayInputSpirv;
|
|
||||||
if (glShaderType == GL_VERTEX_SHADER &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
|
||||||
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
|
||||||
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
|
||||||
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
|
||||||
// re-serialised copy either way, and adopting that copy for every vertex
|
|
||||||
// shader would put every one of them through a round trip they do not need
|
|
||||||
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
|
|
||||||
// first time this gate was missing.
|
|
||||||
effectiveSpirv = &splitArrayInputSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adopt the rewritten module only when THIS stage actually had one of the
|
|
||||||
// blocks - the optimizer hands back a re-serialised copy either way, and taking
|
|
||||||
// that copy for a module it did not rewrite is not free (it cost the
|
|
||||||
// create-indirect retrace 0.15 SSIM when the array-input split first missed
|
|
||||||
// this gate). The report has to be per stage, not cumulative: a fragment shader
|
|
||||||
// consuming the same block reports a name the vertex stage already reported,
|
|
||||||
// and its own rewrite must still be taken or the two stages stop matching.
|
|
||||||
Vector<unsigned int> flattenedXfbSpirv;
|
|
||||||
std::set<String> stageFlattenedXfbBlockNames;
|
|
||||||
if (!xfbCaptureBlockNames.empty() &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
|
||||||
*effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames,
|
|
||||||
flattenedXfbSpirv, enableSpirvValidation) &&
|
|
||||||
!flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) {
|
|
||||||
effectiveSpirv = &flattenedXfbSpirv;
|
|
||||||
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
|
||||||
stageFlattenedXfbBlockNames.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
|
||||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
|
||||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
|
||||||
// ForceSupporterOutput swaps the fragment header to highp, that member reads
|
|
||||||
// back as highp and the ES driver refuses to link ("definitions of uniform
|
|
||||||
// block ... do not match"). Strip the hint from block structs so both stages
|
|
||||||
// declare the member highp; nothing else about emission changes.
|
|
||||||
Vector<unsigned int> uboPrecisionSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
|
||||||
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
|
||||||
!uboPrecisionSpirv.empty()) {
|
|
||||||
effectiveSpirv = &uboPrecisionSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
|
||||||
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
|
||||||
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
|
||||||
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
|
||||||
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
|
||||||
// the decoration - exact, extension-free. Devices that have the extension keep the
|
|
||||||
// decoration and let the hardware do it natively.
|
|
||||||
Vector<unsigned int> noperspectiveSpirv;
|
|
||||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
|
||||||
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
|
||||||
!noperspectiveSpirv.empty()) {
|
|
||||||
effectiveSpirv = &noperspectiveSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
|
||||||
// than approximating one. The shared pass turns the type into the 2D one and
|
|
||||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
|
||||||
// size, which is the whole of the difference between the two.
|
|
||||||
Vector<unsigned int> rectLoweredSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
|
||||||
!rectLoweredSpirv.empty()) {
|
|
||||||
effectiveSpirv = &rectLoweredSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
|
|
||||||
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
|
|
||||||
// as 2D for images without ever asking whether the type is arrayed, so a
|
|
||||||
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
|
|
||||||
// two-component constructor, which every driver rejects, taking the whole
|
|
||||||
// program with it. The pass does the conversion properly - type to 2D array,
|
|
||||||
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
|
||||||
Vector<unsigned int> arrayImageSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
|
||||||
arrayImageSpirv, enableSpirvValidation) &&
|
|
||||||
!arrayImageSpirv.empty()) {
|
|
||||||
effectiveSpirv = &arrayImageSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
|
||||||
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
|
||||||
// format"), which loses the whole program. Give each such image the format the
|
|
||||||
// application bound to its unit - the one GL's format-class rules make correct -
|
|
||||||
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
|
|
||||||
// also rewrites image types, so this one is looking at the final shapes.
|
|
||||||
//
|
//
|
||||||
// Gated on the module actually declaring one: the map is empty for every program
|
// KEYING ON THE RESOLVED MAPS rather than on what they were derived from
|
||||||
// whose images all declare formats, and the cheap probe keeps a program that has
|
// (collidingIoBlockNames, declaredIoBlockNames, stagePipelineIndices, this
|
||||||
// an unbound format-less image from paying an optimizer round trip per stage.
|
// stage's index) is deliberate: the maps ARE the pass's arguments, so they are
|
||||||
Vector<unsigned int> imageFormatSpirv;
|
// exactly as fine as the pass's behaviour and no finer. Two programs whose
|
||||||
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
// collision plans differ but whose maps for THIS stage come out identical really
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
// do produce the same ESSL and should share the entry.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
//
|
||||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
// A block whose other end is NOT in this program is deliberately left out of the
|
||||||
enableSpirvValidation) &&
|
// plan: in a separate-shader-objects pipeline the interface it matches across
|
||||||
!imageFormatSpirv.empty()) {
|
// lives in another program that never saw this plan, and renaming one side of
|
||||||
effectiveSpirv = &imageFormatSpirv;
|
// THAT would break a program pipeline to repair a driver quirk. That is what the
|
||||||
|
// producer/consumer presence tests below are for - in a monolithic program both
|
||||||
|
// are trivially satisfied for every interface the collision can touch.
|
||||||
|
std::map<String, String> inputBlockRenames;
|
||||||
|
std::map<String, String> outputBlockRenames;
|
||||||
|
if (!collidingIoBlockNames.empty() && stagePipelineIndices[index] >= 0) {
|
||||||
|
const Int myPipelineIndex = stagePipelineIndices[index];
|
||||||
|
Int producerPipelineIndex = -1;
|
||||||
|
Bool hasConsumerStage = false;
|
||||||
|
for (const Int otherPipelineIndex : stagePipelineIndices) {
|
||||||
|
if (otherPipelineIndex < 0) continue;
|
||||||
|
if (otherPipelineIndex < myPipelineIndex &&
|
||||||
|
otherPipelineIndex > producerPipelineIndex) {
|
||||||
|
producerPipelineIndex = otherPipelineIndex;
|
||||||
|
}
|
||||||
|
if (otherPipelineIndex > myPipelineIndex) hasConsumerStage = true;
|
||||||
|
}
|
||||||
|
for (const auto& collidingBlockName : collidingIoBlockNames) {
|
||||||
|
if (producerPipelineIndex >= 0) {
|
||||||
|
inputBlockRenames[collidingBlockName] =
|
||||||
|
uniqueIoBlockName(collidingBlockName, producerPipelineIndex);
|
||||||
|
}
|
||||||
|
if (hasConsumerStage) {
|
||||||
|
outputBlockRenames[collidingBlockName] =
|
||||||
|
uniqueIoBlockName(collidingBlockName, myPipelineIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esslKeyInputs.inputBlockRenames = &inputBlockRenames;
|
||||||
|
esslKeyInputs.outputBlockRenames = &outputBlockRenames;
|
||||||
|
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
|
||||||
|
|
||||||
|
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
|
||||||
|
MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) {
|
||||||
|
esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GLSL ES demands a constant integral expression to index a fragment output
|
std::set<String> stageFlattenedXfbBlockNames;
|
||||||
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
|
// Per stage, and NOT m_atomicCounterGlBindings directly: on a miss the
|
||||||
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
|
// transpile appends to this, on a hit the payload supplies it, and only then
|
||||||
// outright ("array indexes for fragment outputs must be constant integral
|
// is it folded into the program-wide vector. Pointing the transpile straight
|
||||||
// expressions"), linking no program and silently no-oping every draw that
|
// at the member would have made the miss path and the hit path disagree about
|
||||||
// uses it. Mesa accepts it, ANGLE does not - which is the whole of the
|
// who owns the append.
|
||||||
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
|
Vector<Int> stageAtomicCounterGlBindings;
|
||||||
// on the ESSL path only: the same module is legal for DirectVulkan.
|
const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
|
||||||
Vector<unsigned int> outputIndexSpirv;
|
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
|
||||||
if (glShaderType == GL_FRAGMENT_SHADER &&
|
if (esslHit) {
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
source = esslHit->essl;
|
||||||
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
|
||||||
!outputIndexSpirv.empty()) {
|
stageAtomicCounterGlBindings = esslHit->atomicCounterGlBindings;
|
||||||
effectiveSpirv = &outputIndexSpirv;
|
} else {
|
||||||
|
String transpileError;
|
||||||
|
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
|
||||||
|
imageFormatBake, storageBlockBindingOverrides,
|
||||||
|
inputBlockRenames, outputBlockRenames,
|
||||||
|
m_atomicCounterEsslBindingTop,
|
||||||
|
enableSpirvValidation, source,
|
||||||
|
stageFlattenedXfbBlockNames,
|
||||||
|
stageAtomicCounterGlBindings, transpileError)) {
|
||||||
|
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
|
||||||
|
// below: one line per failing stage is bounded by program count and
|
||||||
|
// naming the stage is the entire diagnostic value. A stage that never
|
||||||
|
// reaches the driver leaves the program short of that stage, so the link
|
||||||
|
// fails with an EMPTY driver info log - the least debuggable failure
|
||||||
|
// MobileGL can produce, and what hid the whole
|
||||||
|
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
||||||
|
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||||
|
"SPIRV-Cross error: %s",
|
||||||
|
stateProgramObject->GetExternalIndex(),
|
||||||
|
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||||
|
transpileError.c_str());
|
||||||
|
m_backendProgramUsable = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (esslCacheKey.Valid()) {
|
||||||
|
auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
|
||||||
|
payload->essl = source;
|
||||||
|
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
|
||||||
|
payload->atomicCounterGlBindings = stageAtomicCounterGlBindings;
|
||||||
|
const SizeT payloadBytes =
|
||||||
|
MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
|
||||||
|
esslCache.Insert(
|
||||||
|
esslCacheKey,
|
||||||
|
MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)),
|
||||||
|
payloadBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Per stage, never cumulative: a fragment shader consuming the same block
|
||||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
// reports a name the vertex stage already reported, and its own rewrite must
|
||||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
// still be taken or the two stages stop matching. Done here rather than inside
|
||||||
|
// the transpile so a cache HIT contributes its names too.
|
||||||
spvc_compiler_options options;
|
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
||||||
spvcSession.CreateOptions(&options);
|
stageFlattenedXfbBlockNames.end());
|
||||||
|
// Same rule for the atomic-counter bindings this stage declared, and for the
|
||||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION,
|
// same reason: the loop below de-duplicates across stages, so a hit that
|
||||||
ResolveBackendEsslVersion());
|
// contributed nothing would silently drop a counter buffer the draw path has
|
||||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
// to bind.
|
||||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
m_atomicCounterGlBindings.insert(m_atomicCounterGlBindings.end(),
|
||||||
|
stageAtomicCounterGlBindings.begin(),
|
||||||
spvcSession.SetOptions(options);
|
stageAtomicCounterGlBindings.end());
|
||||||
|
|
||||||
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
|
|
||||||
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
|
|
||||||
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
|
|
||||||
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
|
|
||||||
// then deliberately preserves the qualifier for `buffer` declarations.
|
|
||||||
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
|
|
||||||
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* result = nullptr;
|
|
||||||
spvcSession.Compile(&result);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics below:
|
|
||||||
// one line per failing stage is bounded by program count and naming the
|
|
||||||
// stage is the entire diagnostic value. A stage that
|
|
||||||
// never reaches the driver leaves the program short of that stage, so the
|
|
||||||
// link fails with an EMPTY driver info log - the least debuggable failure
|
|
||||||
// MobileGL can produce, and what hid the whole
|
|
||||||
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
|
||||||
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
|
||||||
"SPIRV-Cross error: %s",
|
|
||||||
stateProgramObject->GetExternalIndex(),
|
|
||||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
|
||||||
spvcSession.GetLastErrorString());
|
|
||||||
m_backendProgramUsable = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
source = result;
|
|
||||||
|
|
||||||
// Position in the chain is arbitrary: this is the only header-level rewrite, it
|
// Position in the chain is arbitrary: this is the only header-level rewrite, it
|
||||||
// edits #extension directives and never the body, and the replacement is the
|
// edits #extension directives and never the body, and the replacement is the
|
||||||
@@ -5353,6 +5723,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A counter buffer declared by several stages was recorded once per stage; the draw
|
||||||
|
// path binds per GL binding point, so collapse the duplicates here rather than
|
||||||
|
// re-issuing the same glBindBufferBase two or three times every draw.
|
||||||
|
if (!m_atomicCounterGlBindings.empty()) {
|
||||||
|
std::sort(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end());
|
||||||
|
m_atomicCounterGlBindings.erase(
|
||||||
|
std::unique(m_atomicCounterGlBindings.begin(), m_atomicCounterGlBindings.end()),
|
||||||
|
m_atomicCounterGlBindings.end());
|
||||||
|
}
|
||||||
|
|
||||||
// Transform feedback capture runs on the real driver (see XfbImpl in
|
// Transform feedback capture runs on the real driver (see XfbImpl in
|
||||||
// DirectGLES.cpp), so the capture set has to be declared on the backend
|
// DirectGLES.cpp), so the capture set has to be declared on the backend
|
||||||
// program before it links. SPIRV-Cross keeps user output names verbatim in
|
// program before it links. SPIRV-Cross keeps user output names verbatim in
|
||||||
|
|||||||
@@ -396,6 +396,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||||
void InvalidateIndexedBufferBindingCache();
|
void InvalidateIndexedBufferBindingCache();
|
||||||
|
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
||||||
|
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
||||||
|
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
||||||
|
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
|
||||||
|
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
|
||||||
|
// application bound never reaches the driver.
|
||||||
|
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
|
||||||
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
||||||
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
||||||
// without glDeleteBuffers (called when the ES context is going away).
|
// without glDeleteBuffers (called when the ES context is going away).
|
||||||
@@ -1086,6 +1093,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
|
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
|
||||||
|
struct ImageFormatBakeInputs;
|
||||||
|
|
||||||
class BackendProgramObjectImpl {
|
class BackendProgramObjectImpl {
|
||||||
public:
|
public:
|
||||||
// Per-link cache of a sampler-style uniform's backend location: built once in
|
// Per-link cache of a sampler-style uniform's backend location: built once in
|
||||||
@@ -1171,6 +1181,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// qualifier, so the overrides are baked into the source). A mismatch means the
|
// qualifier, so the overrides are baked into the source). A mismatch means the
|
||||||
// program is stale exactly like the clamp masks above.
|
// program is stale exactly like the clamp masks above.
|
||||||
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
||||||
|
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
|
||||||
|
// and the top of the reserved shader-storage range their counter blocks were
|
||||||
|
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
|
||||||
|
// program that uses no atomic counter, which is what keeps the per-draw cost of the
|
||||||
|
// counter sync at one empty-vector test.
|
||||||
|
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
||||||
|
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
||||||
|
|
||||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||||
@@ -1219,6 +1236,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
private:
|
private:
|
||||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
|
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||||
|
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||||
|
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||||
|
// input it reads must appear in EsslTranslationKeyInputs - see the definition's
|
||||||
|
// header comment in Managers.cpp and MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||||
|
// False means SPIRV-Cross refused the module; `outError` then carries its message.
|
||||||
|
Bool TranspileSpirvToEssl(const Vector<unsigned int>& spirvCode, GLenum glShaderType,
|
||||||
|
const std::set<String>& xfbCaptureBlockNames,
|
||||||
|
const ImageFormatBakeInputs& imageFormatBake,
|
||||||
|
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
||||||
|
String& outSource,
|
||||||
|
std::set<String>& outFlattenedXfbBlockNames,
|
||||||
|
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
Uint m_backendProgramId = 0;
|
||||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||||
@@ -1237,6 +1271,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint m_fragColorBroadcastCount = 1;
|
Uint m_fragColorBroadcastCount = 1;
|
||||||
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
||||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||||
|
Vector<Int> m_atomicCounterGlBindings;
|
||||||
|
Int m_atomicCounterEsslBindingTop = -1;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Bool m_backendProgramUsable = false;
|
Bool m_backendProgramUsable = false;
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
}
|
}
|
||||||
|
// 8-bit signed-normalized storage is core ES, so only the rendering half is in
|
||||||
|
// question here; the 16-bit bit above additionally needs EXT_texture_norm16 for the
|
||||||
|
// encoding to exist at all.
|
||||||
|
if (!capabilities.SupportsRenderSnorm) {
|
||||||
|
options |= PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,9 +828,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
||||||
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
||||||
// comes out of here, whichever order the two passes end up running in.
|
// comes out of here, whichever order the two passes end up running in.
|
||||||
|
//
|
||||||
|
// `forceCoherent` is for the SPLIT pair only. GLSL guarantees that a write through
|
||||||
|
// one image variable is visible to a read through a DIFFERENT one only when both are
|
||||||
|
// declared coherent, and the split turns a same-variable read-after-write - which
|
||||||
|
// desktop GLSL orders by construction, so the source almost never says `coherent` -
|
||||||
|
// into exactly that cross-variable shape. Without it the driver may serve the load
|
||||||
|
// from a cache that never saw the store through the writeonly half.
|
||||||
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
||||||
const String& variableName) {
|
const String& variableName, Bool forceCoherent = false) {
|
||||||
String out = "layout(" + decl.layout + ") uniform ";
|
String out = "layout(" + decl.layout + ") uniform ";
|
||||||
|
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
|
||||||
|
out += "coherent ";
|
||||||
|
}
|
||||||
out += memoryQualifier;
|
out += memoryQualifier;
|
||||||
out += ' ';
|
out += ' ';
|
||||||
if (!decl.qualifiers.empty()) {
|
if (!decl.qualifiers.empty()) {
|
||||||
@@ -1008,9 +1024,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||||
takenAliases.push_back(decl.writeName);
|
takenAliases.push_back(decl.writeName);
|
||||||
decl.split = true;
|
decl.split = true;
|
||||||
|
// Both halves carry `coherent`; see BuildImageDeclaration. The
|
||||||
|
// single-declaration cases below stay as they were - nothing aliases them, so
|
||||||
|
// there is no visibility to restore and no reason to pay for the cache
|
||||||
|
// behaviour.
|
||||||
edits.push_back({decl.declStart, decl.declLength,
|
edits.push_back({decl.declStart, decl.declLength,
|
||||||
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
|
BuildImageDeclaration(decl, "readonly", decl.name, /*forceCoherent=*/true) +
|
||||||
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
|
"\n" +
|
||||||
|
BuildImageDeclaration(decl, "writeonly", decl.writeName,
|
||||||
|
/*forceCoherent=*/true)});
|
||||||
} else if (decl.stored) {
|
} else if (decl.stored) {
|
||||||
edits.push_back({decl.declStart, decl.declLength,
|
edits.push_back({decl.declStart, decl.declLength,
|
||||||
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||||
|
|||||||
@@ -196,11 +196,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// * loaded only -> add `readonly`
|
// * loaded only -> add `readonly`
|
||||||
// * stored only -> add `writeonly`
|
// * stored only -> add `writeonly`
|
||||||
// * both -> emit TWO declarations on the same binding and of the
|
// * both -> emit TWO declarations on the same binding and of the
|
||||||
// same type, `readonly <name>` and `writeonly
|
// same type, `coherent readonly <name>` and `coherent
|
||||||
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
|
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point
|
||||||
// imageStore at the second one. Several image variables
|
// every imageStore at the second one. Several image
|
||||||
// may share an image unit as long as they have the same
|
// variables may share an image unit as long as they have
|
||||||
// type and format, which is exactly what the pair is.
|
// the same type and format, which is exactly what the pair
|
||||||
|
// is.
|
||||||
|
//
|
||||||
|
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||||
|
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||||
|
// one when both are coherent, and the split is what makes a same-variable
|
||||||
|
// read-after-write cross-variable. The single-declaration repairs above do not get it -
|
||||||
|
// nothing aliases them.
|
||||||
//
|
//
|
||||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||||
|
|||||||
@@ -905,8 +905,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||||
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
||||||
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
// Same shape as the image-uniform limits three lines above: maxClipDistances is reported
|
||||||
|
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
|
||||||
|
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
|
||||||
|
// 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;
|
||||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
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
|
||||||
|
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
|
||||||
|
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
|
||||||
|
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
|
||||||
|
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
|
||||||
|
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
|
||||||
|
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -330,6 +331,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||||
// so they always agree without extra plumbing.
|
// so they always agree without extra plumbing.
|
||||||
|
//
|
||||||
|
// ... as long as the shader half still runs. It does not when the backend has declared
|
||||||
|
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
|
||||||
|
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
|
||||||
|
// and a UINT-formatted attribute would be fed to a float input - garbage with no
|
||||||
|
// diagnostic anywhere. Declining here drops the array instead (the caller skips
|
||||||
|
// UNDEFINED attributes and reports them through unsupportedAttribMask), which is what
|
||||||
|
// DirectGLES does for the same state. The frontend RECORDS the format either way, so
|
||||||
|
// this gate is the only thing standing between a legal glVertexAttribLFormat and a
|
||||||
|
// mismatched pipeline.
|
||||||
|
if (MG_Backend::pActiveBackendObject == nullptr ||
|
||||||
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 1: return VK_FORMAT_R32G32_UINT;
|
case 1: return VK_FORMAT_R32G32_UINT;
|
||||||
|
|||||||
@@ -1494,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||||
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||||
|
resource.syncedShapeVersion != texture.GetShapeVersion() ||
|
||||||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||||
resource.syncedMipLevelCount != mipLevelCount;
|
resource.syncedMipLevelCount != mipLevelCount;
|
||||||
}
|
}
|
||||||
@@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
TextureResource &outResource) {
|
TextureResource &outResource) {
|
||||||
// Cross-draw fast path: if the resource is already built and neither the texture's
|
// Cross-draw fast path: if the resource is already built and neither the texture's
|
||||||
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last
|
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
|
||||||
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
|
// nor its params changed since the last sync, there is nothing to re-check or
|
||||||
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
|
// re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
|
||||||
// maintained separately by the transition path, so the resource still reflects truth.
|
// the per-level dirty scan. Layout is maintained separately by the transition path, so
|
||||||
|
// the resource still reflects truth. The shape version is NOT redundant with the
|
||||||
|
// content one: glTexImage2D(..., nullptr) re-specifies a level's size or format
|
||||||
|
// without dirtying a texel, which is exactly how a re-specified image-unit texture used
|
||||||
|
// to keep reporting its old imageSize().
|
||||||
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
||||||
|
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
|
||||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 syncingMipLevelCount =
|
const Uint32 syncingMipLevelCount =
|
||||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||||
@@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||||
outResource.syncedContentVersion == syncingContentVersion &&
|
outResource.syncedContentVersion == syncingContentVersion &&
|
||||||
|
outResource.syncedShapeVersion == syncingShapeVersion &&
|
||||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1629,6 +1636,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves
|
||||||
|
// out of the height it occupies GL-side and into z, which is the slot
|
||||||
|
// TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves
|
||||||
|
// extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have.
|
||||||
|
texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize);
|
||||||
|
|
||||||
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
||||||
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
||||||
return false;
|
return false;
|
||||||
@@ -1660,6 +1673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!hasDirtyMipLevel) {
|
if (!hasDirtyMipLevel) {
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
|
outResource.syncedShapeVersion = syncingShapeVersion;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1669,6 +1683,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
|
outResource.syncedShapeVersion = syncingShapeVersion;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2536,7 +2551,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
uploadItem.target = target;
|
uploadItem.target = target;
|
||||||
uploadItem.level = level;
|
uploadItem.level = level;
|
||||||
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
||||||
uploadItem.texelSize = texelSize;
|
// Vulkan geometry, like the image this stages into (see SyncTexture): a 1D
|
||||||
|
// array's layers move from y to z, where the copy loop's depthSelectsArrayLayer
|
||||||
|
// branch turns them into layerCount. The shadow needs no repacking to follow -
|
||||||
|
// one layer of a 1D array IS one row of `width` texels, so the tight-packed
|
||||||
|
// per-layer copy the swapped size describes reads the same bytes in the same
|
||||||
|
// order as the row-major level it replaces.
|
||||||
|
uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize);
|
||||||
uploadItem.source = source;
|
uploadItem.source = source;
|
||||||
uploadItem.offset = stagingSize;
|
uploadItem.offset = stagingSize;
|
||||||
uploadItem.uploadByteSize = byteSize;
|
uploadItem.uploadByteSize = byteSize;
|
||||||
@@ -2574,6 +2595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
||||||
}
|
}
|
||||||
|
// The boxes came out of the shadow in GL coordinates, where a 1D
|
||||||
|
// array's layer is the y. They have to follow texelSize across to z or
|
||||||
|
// they would address rows of an image that now has exactly one, and
|
||||||
|
// the staging walk would read the wrong bytes for them. Every byte
|
||||||
|
// count computed above is a product of the three extents, so moving
|
||||||
|
// the axes leaves all of them alone - and an OFFSET lands on a zero y,
|
||||||
|
// not on the extent's one, which is why this is spelled out rather than
|
||||||
|
// handed to ToVulkanLevelExtent.
|
||||||
|
if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) {
|
||||||
|
uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()};
|
||||||
|
uploadItem.regionSize = {uploadItem.regionSize.x(), 1,
|
||||||
|
uploadItem.regionSize.y()};
|
||||||
|
for (auto& rect : uploadItem.rects) {
|
||||||
|
rect.lo = {rect.lo.x(), 0, rect.lo.y()};
|
||||||
|
rect.hi = {rect.hi.x(), 1, rect.hi.y()};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (formatInfo.expandRgbToRgba) {
|
if (formatInfo.expandRgbToRgba) {
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ class ITextureObject;
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
enum class SamplerNumericDomain : Uint8;
|
enum class SamplerNumericDomain : Uint8;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
|
||||||
|
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
|
||||||
|
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
|
||||||
|
// level size into Vulkan image geometry has to move the count across first, and every GL-space
|
||||||
|
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
|
||||||
|
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
|
||||||
|
//
|
||||||
|
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
|
||||||
|
// depth/layer count in z, which is where the Vulkan side expects it.
|
||||||
|
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
|
||||||
|
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||||
|
return {glTexelSize.x(), 1, glTexelSize.y()};
|
||||||
|
}
|
||||||
|
return glTexelSize;
|
||||||
|
}
|
||||||
|
|
||||||
class VkTextureManager {
|
class VkTextureManager {
|
||||||
public:
|
public:
|
||||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||||
@@ -206,6 +225,12 @@ public:
|
|||||||
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
||||||
// busts the skip even if it failed to bump the content version.
|
// busts the skip even if it failed to bump the content version.
|
||||||
Uint32 syncedMipLevelCount = 0;
|
Uint32 syncedMipLevelCount = 0;
|
||||||
|
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
|
||||||
|
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
|
||||||
|
// already-defined level changes its size or format and dirties no texel, so it moves the
|
||||||
|
// shape version and nothing else. Without this in the early-out key the image, its views
|
||||||
|
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
|
||||||
|
Uint64 syncedShapeVersion = 0;
|
||||||
|
|
||||||
TextureResource() = default;
|
TextureResource() = default;
|
||||||
TextureResource(const TextureResource&) = delete;
|
TextureResource(const TextureResource&) = delete;
|
||||||
@@ -237,6 +262,7 @@ public:
|
|||||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
|
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reset() {
|
void Reset() {
|
||||||
@@ -300,6 +326,7 @@ public:
|
|||||||
syncedTextureParamsVersion = 0;
|
syncedTextureParamsVersion = 0;
|
||||||
syncedContentVersion = 0;
|
syncedContentVersion = 0;
|
||||||
syncedMipLevelCount = 0;
|
syncedMipLevelCount = 0;
|
||||||
|
syncedShapeVersion = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
~TextureResource() {
|
~TextureResource() {
|
||||||
|
|||||||
@@ -9813,7 +9813,7 @@ void main() {
|
|||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
||||||
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height, GLenum format, GLenum type, void* pixels,
|
GLsizei height, GLenum format, GLenum type, void* pixels,
|
||||||
Bool defaultFramebufferOrientation) {
|
Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) {
|
||||||
const Bool wantDepth = format != GL_STENCIL_INDEX;
|
const Bool wantDepth = format != GL_STENCIL_INDEX;
|
||||||
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
@@ -9892,6 +9892,10 @@ void main() {
|
|||||||
if (!mapped) return;
|
if (!mapped) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See the header: a stack of one-row layers and a single multi-row layer copy out to the
|
||||||
|
// same tightly-packed bytes, so only the region's shape splits the two cases.
|
||||||
|
const Uint32 copyLayerCount = std::max<Uint32>(sourceLayerCount, 1u);
|
||||||
|
const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height;
|
||||||
VkBufferImageCopy regions[2]{};
|
VkBufferImageCopy regions[2]{};
|
||||||
Uint32 regionCount = 0;
|
Uint32 regionCount = 0;
|
||||||
if (wantDepth) {
|
if (wantDepth) {
|
||||||
@@ -9900,9 +9904,9 @@ void main() {
|
|||||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
region.imageSubresource.layerCount = copyLayerCount;
|
||||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
region.imageExtent = {copyExtent.width, copyRowCount, 1};
|
||||||
}
|
}
|
||||||
if (wantStencil) {
|
if (wantStencil) {
|
||||||
auto& region = regions[regionCount++];
|
auto& region = regions[regionCount++];
|
||||||
@@ -9910,9 +9914,9 @@ void main() {
|
|||||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = 1;
|
region.imageSubresource.layerCount = copyLayerCount;
|
||||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
region.imageExtent = {copyExtent.width, copyRowCount, 1};
|
||||||
}
|
}
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||||
regionCount, regions);
|
regionCount, regions);
|
||||||
@@ -10147,9 +10151,17 @@ void main() {
|
|||||||
? static_cast<Uint32>(textureUploadTarget) -
|
? static_cast<Uint32>(textureUploadTarget) -
|
||||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
|
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
|
||||||
: 0;
|
: 0;
|
||||||
|
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
||||||
|
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
||||||
|
// rows of layer 0, so the read has to be told which of the two it is looking at.
|
||||||
|
const Uint32 sourceLayers =
|
||||||
|
textureObject->GetTarget() == TextureTarget::Texture1DArray
|
||||||
|
? static_cast<Uint32>(std::max<Int>(levelSize.y(), 1))
|
||||||
|
: 1u;
|
||||||
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
|
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
|
||||||
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
|
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
|
||||||
levelSize.y(), format, type, pixels);
|
levelSize.y(), format, type, pixels,
|
||||||
|
/*defaultFramebufferOrientation=*/false, sourceLayers);
|
||||||
} else {
|
} else {
|
||||||
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
|
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
|
||||||
}
|
}
|
||||||
@@ -10167,12 +10179,19 @@ void main() {
|
|||||||
// destination layout (GL 3.3 section 6.1.4).
|
// destination layout (GL 3.3 section 6.1.4).
|
||||||
const auto imageTextureTarget = textureObject->GetTarget();
|
const auto imageTextureTarget = textureObject->GetTarget();
|
||||||
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
||||||
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
|
const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray;
|
||||||
|
const Bool isArrayImage = is1dArrayImage ||
|
||||||
imageTextureTarget == TextureTarget::Texture2DArray ||
|
imageTextureTarget == TextureTarget::Texture2DArray ||
|
||||||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||||
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
||||||
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
||||||
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
|
// A 1D array level comes back as ONE two-dimensional image whose rows are its layers
|
||||||
|
// (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not
|
||||||
|
// multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps
|
||||||
|
// them in arrayLayers on a one-row image, which is what the copy region below says - the
|
||||||
|
// two describe the same tightly-packed bytes.
|
||||||
|
const GLsizei sliceCount =
|
||||||
|
std::max<GLsizei>(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1);
|
||||||
if (bufSize >= 0) {
|
if (bufSize >= 0) {
|
||||||
const Int dstChannels = GetReadbackChannelCount(format);
|
const Int dstChannels = GetReadbackChannelCount(format);
|
||||||
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
||||||
@@ -10225,7 +10244,8 @@ void main() {
|
|||||||
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
||||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
|
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
||||||
|
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
||||||
static_cast<Uint32>(depthSlices)};
|
static_cast<Uint32>(depthSlices)};
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
readback.GetHandle(), 1, ©Region);
|
readback.GetHandle(), 1, ©Region);
|
||||||
|
|||||||
@@ -217,10 +217,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
||||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
||||||
// the way back, exactly as the colour ReadPixels path does.
|
// the way back, exactly as the colour ReadPixels path does.
|
||||||
|
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
|
||||||
|
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
|
||||||
|
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
|
||||||
|
// only the copy region differs; everything after it is written against `height`.
|
||||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels, Bool defaultFramebufferOrientation = false);
|
void* pixels, Bool defaultFramebufferOrientation = false,
|
||||||
|
Uint32 sourceLayerCount = 1);
|
||||||
// Same-extent depth blit between images of different depth formats: host
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
|
|||||||
@@ -44,4 +44,5 @@ add_subdirectory(Program)
|
|||||||
add_subdirectory(Buffer)
|
add_subdirectory(Buffer)
|
||||||
add_subdirectory(Driver)
|
add_subdirectory(Driver)
|
||||||
add_subdirectory(Container)
|
add_subdirectory(Container)
|
||||||
|
add_subdirectory(ShaderCache)
|
||||||
add_subdirectory(Transpile)
|
add_subdirectory(Transpile)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
TranslationCacheBench
|
||||||
|
TranslationCacheBench.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(TranslationCacheBench PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
TranslationCacheBench PRIVATE
|
||||||
|
benchmark::benchmark
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME TranslationCacheBench COMMAND TranslationCacheBench --benchmark_counters_tabular=true)
|
||||||
|
set_tests_properties(TranslationCacheBench PROPERTIES LABELS benchmark)
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.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
|
||||||
|
|
||||||
|
// What the two-level shader translation memo is worth, measured on the workload that
|
||||||
|
// motivated it: the KHR-GL33.texture_swizzle.smoke_* shape, where one case builds 2592
|
||||||
|
// programs out of a handful of distinct sources.
|
||||||
|
//
|
||||||
|
// Four pairs of cases, each Off/On:
|
||||||
|
//
|
||||||
|
// ProgramLink - the whole glCompileShader + glLinkProgram path for one program, with
|
||||||
|
// FRESH SHADER OBJECTS every iteration. This is the CTS shape exactly,
|
||||||
|
// and it is the headline case now. It used to be the PESSIMISTIC one:
|
||||||
|
// a hit still paid for both glslang parses, because the parse happens
|
||||||
|
// at glCompileShader - a different entry point from the one L1
|
||||||
|
// memoizes - and fresh shader objects meant ShaderCompileAdoptionMap
|
||||||
|
// could not hand the earlier parse over either. L1c is what closed
|
||||||
|
// that: the compile half of the memo recognises each stage's source
|
||||||
|
// and publishes its verdict without parsing, so on a hit this case now
|
||||||
|
// constructs no glslang object at all.
|
||||||
|
//
|
||||||
|
// SharedShaderLink - the same program population with the shader objects KEPT ALIVE, so
|
||||||
|
// the parses happen once outside the measured loop whatever the cache
|
||||||
|
// does. That makes it the CONTROL for L1c rather than a target: its
|
||||||
|
// numbers should not move, and if they do, L1c has added cost to a
|
||||||
|
// path it was supposed to leave alone.
|
||||||
|
//
|
||||||
|
// DeferredParseLink - the shape where L1c could LOSE: a constant vertex source (which
|
||||||
|
// hits L1c and therefore skips its parse) against a fresh fragment
|
||||||
|
// source every iteration (which makes the PROGRAM key miss, so the
|
||||||
|
// skipped parse has to happen inside the link after all). Same parse
|
||||||
|
// count either way, so the pair should land within noise; see its own
|
||||||
|
// header below.
|
||||||
|
//
|
||||||
|
// EsslTranspile - the DirectGLES backend segment: the SPIR-V pass chain plus
|
||||||
|
// SPIRV-Cross. Runs the driver-INDEPENDENT half of the real chain (the
|
||||||
|
// passes SyncToBackend runs unconditionally, plus the two stage-gated
|
||||||
|
// ones a fragment module reaches) so the miss path costs what
|
||||||
|
// production costs; the capability-gated passes need a live ES driver
|
||||||
|
// and are not reachable from a benchmark process.
|
||||||
|
//
|
||||||
|
// Every On case runs with a warm cache: the first iteration misses and every one after it
|
||||||
|
// hits, which is exactly the steady state of a 2592-program smoke case.
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
|
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
|
||||||
|
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char* kVertexSource = R"(#version 460
|
||||||
|
layout(location = 0) in vec3 aPos;
|
||||||
|
out vec3 vPos;
|
||||||
|
out vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vPos = aPos;
|
||||||
|
vUv = aPos.xy * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(aPos, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Shaped after gl3cTextureSwizzleTests.cpp's template: a sampler of one type, one
|
||||||
|
// TEXTURE_ACCESS, one CHANNEL, and an output whose BASIC_TYPE is the only thing that
|
||||||
|
// varies within a case. Padded with enough real arithmetic that the translation chain
|
||||||
|
// is doing work rather than measuring fixed overheads.
|
||||||
|
// `padLines` = 0 is the honest CTS size: gl3cTextureSwizzleTests' smoke template is a
|
||||||
|
// handful of lines, and that is the workload the memo exists for. The padded variant is
|
||||||
|
// kept alongside it because a shaderpack stage is orders of magnitude bigger, and the
|
||||||
|
// two bracket the ratio the cache is worth in practice.
|
||||||
|
String SwizzleLikeFragment(const String& prefix, const int padLines) {
|
||||||
|
String source = "#version 460\n";
|
||||||
|
source += "in vec3 vPos;\n";
|
||||||
|
source += "in vec2 vUv;\n";
|
||||||
|
source += "layout(location = 0) out " + prefix + "vec4 fragColor;\n";
|
||||||
|
source += "uniform sampler2D uTex;\n";
|
||||||
|
source += "uniform vec4 uTint;\n";
|
||||||
|
source += "uniform mat4 uModel;\n";
|
||||||
|
source += "uniform float uArr[8];\n";
|
||||||
|
source += "void main() {\n";
|
||||||
|
source += " vec4 s = texture(uTex, vUv);\n";
|
||||||
|
source += " float acc = s.r;\n";
|
||||||
|
for (int i = 0; i < padLines; ++i) {
|
||||||
|
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
||||||
|
}
|
||||||
|
source += " for (int i = 0; i < 8; ++i) acc += uArr[i];\n";
|
||||||
|
source += " vec4 p = uModel * vec4(vPos, 1.0);\n";
|
||||||
|
source += " fragColor = " + prefix + "vec4((s + uTint) * acc + p);\n";
|
||||||
|
source += "}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheModeScope {
|
||||||
|
public:
|
||||||
|
explicit CacheModeScope(const Bool enabled)
|
||||||
|
: m_saved(MG_Config::Features.ShaderTranslationCache) {
|
||||||
|
MG_Config::Features.ShaderTranslationCache =
|
||||||
|
enabled ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~CacheModeScope() { MG_Config::Features.ShaderTranslationCache = m_saved; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MG_Config::QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SyncCompileScope {
|
||||||
|
public:
|
||||||
|
SyncCompileScope() : m_saved(MG_Config::Features.AsyncShaderCompile) {
|
||||||
|
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~SyncCompileScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MG_Config::QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One program, built the way the CTS builds one: fresh shader objects every time.
|
||||||
|
void LinkOneProgram(const String& vertexSource, const String& fragmentSource) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
const char* vsText = vertexSource.c_str();
|
||||||
|
ShaderSource(vs, 1, &vsText, nullptr);
|
||||||
|
CompileShader(vs);
|
||||||
|
|
||||||
|
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
const char* fsText = fragmentSource.c_str();
|
||||||
|
ShaderSource(fs, 1, &fsText, nullptr);
|
||||||
|
CompileShader(fs);
|
||||||
|
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, vs);
|
||||||
|
AttachShader(program, fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
benchmark::DoNotOptimize(program);
|
||||||
|
|
||||||
|
DeleteProgram(program);
|
||||||
|
DeleteShader(vs);
|
||||||
|
DeleteShader(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<Uint32> BuildSanitizedFragmentSpirv(const String& fragmentSource) {
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
|
||||||
|
auto shader = ShaderCompiler::CompileShader(attrib);
|
||||||
|
if (!shader) return {};
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shader.value()}};
|
||||||
|
auto program = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!program) return {};
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program.value()};
|
||||||
|
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!binary || binary->empty()) return {};
|
||||||
|
Vector<Uint32> sanitized;
|
||||||
|
if (!ShaderCompiler::SanitizeAndOptimizeBinary(binary->front(), sanitized)) return {};
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The driver-independent part of BackendProgramObjectImpl::TranspileSpirvToEssl, in the
|
||||||
|
// same order. What is missing is only the capability-gated passes (viewport lowering,
|
||||||
|
// multisample clamping, noperspective emulation, the image-format bake), which cannot
|
||||||
|
// fire without a live ES driver to arm them.
|
||||||
|
Bool TranspileLikeDirectGles(const Vector<Uint32>& spirv, const Uint esslVersion, String& outEssl) {
|
||||||
|
Vector<Uint32> a;
|
||||||
|
const Vector<Uint32>* effective = &spirv;
|
||||||
|
if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*effective, a, false) && !a.empty()) {
|
||||||
|
effective = &a;
|
||||||
|
}
|
||||||
|
Vector<Uint32> b;
|
||||||
|
if (ShaderCompiler::LowerRectImages(*effective, b, false) && !b.empty()) effective = &b;
|
||||||
|
Vector<Uint32> c;
|
||||||
|
if (ShaderCompiler::Lower1DArrayImagesForEssl(*effective, c, false) && !c.empty()) effective = &c;
|
||||||
|
Vector<Uint32> d;
|
||||||
|
if (ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*effective, d, false) && !d.empty()) {
|
||||||
|
effective = &d;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpvcSession session(*effective, SessionUsageBit::Transpile);
|
||||||
|
spvc_compiler_options options;
|
||||||
|
if (session.CreateOptions(&options) != SPVC_SUCCESS) return false;
|
||||||
|
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, esslVersion);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||||
|
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||||
|
session.SetOptions(options);
|
||||||
|
const char* result = nullptr;
|
||||||
|
session.Compile(&result);
|
||||||
|
if (!result) return false;
|
||||||
|
outEssl = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EsslTranslationKeyInputs EsslInputsFor(const Vector<Uint32>& spirv) {
|
||||||
|
EsslTranslationKeyInputs inputs;
|
||||||
|
inputs.spirv = &spirv;
|
||||||
|
inputs.shaderType = GL_FRAGMENT_SHADER;
|
||||||
|
inputs.maxColorTextureSamples = 4;
|
||||||
|
inputs.maxIntegerSamples = 1;
|
||||||
|
inputs.maxDepthTextureSamples = 4;
|
||||||
|
inputs.advertisedMaxSamples = 4;
|
||||||
|
inputs.esslVersion = 320;
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1, in situ: the full glCompileShader + glLinkProgram path for a repeated program.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// Arg(0) = the CTS smoke size; Arg(120) = a heavy stage, bracketing the ratio.
|
||||||
|
static void BM_ProgramLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, fs);
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_ProgramLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_ProgramLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
||||||
|
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, fs);
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
// Two stages per iteration, so a clean run shows L1c_hits == 2 * iterations and zero
|
||||||
|
// misses: every glCompileShader in the loop skipped its parse.
|
||||||
|
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
||||||
|
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
|
||||||
|
//
|
||||||
|
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
|
||||||
|
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
|
||||||
|
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
|
||||||
|
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
|
||||||
|
// leaves is the parse, not the link.
|
||||||
|
//
|
||||||
|
// This pair keeps the shader objects alive, so the parses happen once before the measured
|
||||||
|
// loop and the L1 hit then skips the link, mapIO, the SPIR-V, the reflection and the routing
|
||||||
|
// outright.
|
||||||
|
//
|
||||||
|
// SINCE L1c THIS IS THE CONTROL, NOT THE TARGET. Nothing inside the measured loop calls
|
||||||
|
// glCompileShader, so L1c cannot fire here at all - which is exactly what makes the pair
|
||||||
|
// useful: it is the shape that says whether the compile-side memo has slowed the LINK path
|
||||||
|
// down. Its numbers should be indistinguishable from the pre-L1c ones.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
namespace {
|
||||||
|
struct SharedShaders {
|
||||||
|
GLuint vs = 0;
|
||||||
|
GLuint fs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
SharedShaders shaders;
|
||||||
|
shaders.vs = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
const char* vsText = vertexSource.c_str();
|
||||||
|
ShaderSource(shaders.vs, 1, &vsText, nullptr);
|
||||||
|
CompileShader(shaders.vs);
|
||||||
|
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
const char* fsText = fragmentSource.c_str();
|
||||||
|
ShaderSource(shaders.fs, 1, &fsText, nullptr);
|
||||||
|
CompileShader(shaders.fs);
|
||||||
|
return shaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinkFromSharedShaders(const SharedShaders& shaders) {
|
||||||
|
using namespace MG_Impl::GLImpl;
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, shaders.vs);
|
||||||
|
AttachShader(program, shaders.fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
benchmark::DoNotOptimize(program);
|
||||||
|
DeleteProgram(program);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const SharedShaders shaders =
|
||||||
|
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkFromSharedShaders(shaders);
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const SharedShaders shaders =
|
||||||
|
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkFromSharedShaders(shaders);
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
static void BM_EsslTranspile_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const Vector<Uint32> spirv =
|
||||||
|
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
if (spirv.empty()) {
|
||||||
|
state.SkipWithError("could not build the fragment module");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String essl;
|
||||||
|
for (auto _ : state) {
|
||||||
|
if (!TranspileLikeDirectGles(spirv, 320, essl)) {
|
||||||
|
state.SkipWithError("transpile failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(essl.data());
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_EsslTranspile_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_EsslTranspile_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const Vector<Uint32> spirv =
|
||||||
|
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
||||||
|
if (spirv.empty()) {
|
||||||
|
state.SkipWithError("could not build the fragment module");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BoundedTranslationCache<EsslTranslationResult> cache("bench L2", 64, 8u << 20);
|
||||||
|
const EsslTranslationKeyInputs inputs = EsslInputsFor(spirv);
|
||||||
|
for (auto _ : state) {
|
||||||
|
const TranslationCacheKey key = BuildEsslTranslationKey(inputs);
|
||||||
|
EsslTranslationResultPtr hit = cache.Find(key);
|
||||||
|
if (!hit) {
|
||||||
|
auto payload = MakeShared<EsslTranslationResult>();
|
||||||
|
if (!TranspileLikeDirectGles(spirv, inputs.esslVersion, payload->essl)) {
|
||||||
|
state.SkipWithError("transpile failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cache.Insert(key, EsslTranslationResultPtr(payload), EsslTranslationResultBytes(*payload));
|
||||||
|
hit = payload;
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(hit->essl.data());
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = cache.Stats();
|
||||||
|
state.counters["L2_hits"] = static_cast<double>(stats.hits);
|
||||||
|
state.counters["L2_misses"] = static_cast<double>(stats.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_EsslTranspile_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// L1c, the shape where it could LOSE rather than win: the DEFERRED PARSE.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// A stage whose compile hits L1c holds no AST, so if the program-level key then MISSES, the
|
||||||
|
// parse it skipped has to happen anyway - inside the link, via ClaimParsedShader. The parse
|
||||||
|
// is moved, not removed, and this pair is what says whether moving it costs anything.
|
||||||
|
//
|
||||||
|
// The shape forces exactly that, every iteration: one CONSTANT vertex source (hits L1c after
|
||||||
|
// the first iteration) linked against a FRESH fragment source each time (misses L1c, and
|
||||||
|
// makes the program key miss too). So:
|
||||||
|
//
|
||||||
|
// cache off - two parses at glCompileShader, then the link.
|
||||||
|
// cache on - one parse at glCompileShader (the fragment), one deferred parse inside the
|
||||||
|
// link (the vertex), then the link.
|
||||||
|
//
|
||||||
|
// The parse count is identical, so these two should land within noise of each other. If the
|
||||||
|
// On arm is materially SLOWER, L1c is charging for something - the per-compile key build and
|
||||||
|
// hash over the full preprocessed source, or the loss of the claim-CAS reuse - and that cost
|
||||||
|
// shows up here and nowhere else.
|
||||||
|
//
|
||||||
|
// The distinct fragment sources also churn both front-end levels through their FIFO caps,
|
||||||
|
// which is the eviction behaviour a real shaderpack load produces; over a long run the
|
||||||
|
// constant vertex entry is occasionally evicted by that churn and re-inserted, so the L1c
|
||||||
|
// hit rate reported below is high but not exactly 1.0 per iteration.
|
||||||
|
namespace {
|
||||||
|
String UniqueFragmentSource(const Uint64 serial, const int padLines) {
|
||||||
|
return SwizzleLikeFragment("", padLines) +
|
||||||
|
"\n// unique-" + std::to_string(serial) + "\n";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static void BM_DeferredParseLink_CacheOff(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(false);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
Uint64 serial = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
||||||
|
}
|
||||||
|
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_DeferredParseLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
static void BM_DeferredParseLink_CacheOn(benchmark::State& state) {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
const SyncCompileScope sync;
|
||||||
|
const CacheModeScope cache(true);
|
||||||
|
const String vs = kVertexSource;
|
||||||
|
Uint64 serial = 0;
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(~0ull, static_cast<int>(state.range(0)))); // prime the vertex entry
|
||||||
|
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
||||||
|
for (auto _ : state) {
|
||||||
|
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
||||||
|
}
|
||||||
|
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
||||||
|
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
||||||
|
// Expected shape: L1 all misses (every program is new), L1c one hit (vertex) and one miss
|
||||||
|
// (fragment) per iteration.
|
||||||
|
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
||||||
|
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
||||||
|
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
||||||
|
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_DeferredParseLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||||
Bool ValidateBufferTarget(BufferTarget target) {
|
Bool ValidateBufferTarget(BufferTarget target) {
|
||||||
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
pointCount = std::min<SizeT>(pointCount, 4);
|
||||||
}
|
}
|
||||||
|
if (target == BufferTarget::AtomicCounter) {
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
|
||||||
|
// size: a counter buffer reaches a shader only as a lowered storage block, so the
|
||||||
|
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
|
||||||
|
pointCount = std::min<SizeT>(
|
||||||
|
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||||
|
}
|
||||||
return pointCount;
|
return pointCount;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -326,10 +326,23 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
||||||
|
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
||||||
|
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
||||||
|
// everything a real driver would still do inside the block - argument validation and the
|
||||||
|
// errors it raises - happens exactly as it does outside one, and only the command itself is
|
||||||
|
// dropped. It is deliberately not on the frontend's transform-feedback accounting either:
|
||||||
|
// that mirrors what the capture stage would have written, and a conditional block around a
|
||||||
|
// capturing draw has no test coverage in either direction.
|
||||||
|
static Bool ConditionalRenderDiscardsCommand() {
|
||||||
|
return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
|
||||||
|
}
|
||||||
|
|
||||||
void Clear_Backend(GLbitfield mask) {
|
void Clear_Backend(GLbitfield mask) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +350,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +359,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +368,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -361,6 +377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +385,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +394,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +403,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +411,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +420,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||||
maxdrawcount, stride);
|
maxdrawcount, stride);
|
||||||
}
|
}
|
||||||
@@ -408,6 +430,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||||
stride);
|
stride);
|
||||||
}
|
}
|
||||||
@@ -417,6 +440,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -426,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +460,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -444,6 +470,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -453,6 +480,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||||
instancecount, baseinstance);
|
instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -462,6 +490,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +498,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||||
}
|
}
|
||||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
@@ -476,6 +506,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||||
baseinstance);
|
baseinstance);
|
||||||
}
|
}
|
||||||
@@ -484,6 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +523,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +552,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
|
||||||
|
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +606,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
|
if (ConditionalRenderDiscardsCommand()) return;
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender)
|
DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
||||||
@@ -982,7 +982,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdoub
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
|
|||||||
@@ -2613,18 +2613,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
|
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
||||||
|
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
@@ -46,13 +47,29 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr GLint kFrontendMaxComputeUniformComponents = 1024;
|
// Shared with the glslang resource table for the same reason as the atomic-counter
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounters = 8;
|
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8;
|
constexpr GLint kFrontendMaxComputeUniformComponents =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
|
||||||
|
// Every atomic-counter limit is shared with the glslang resource table
|
||||||
|
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
|
||||||
|
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
|
||||||
|
// used to be independent tables that disagreed on both the binding count and the buffer
|
||||||
|
// size. Never move one of these without the other.
|
||||||
|
constexpr GLint kFrontendMaxComputeAtomicCounters =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
||||||
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
||||||
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8;
|
constexpr GLint kFrontendMaxCombinedAtomicCounters =
|
||||||
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8;
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxFragmentAtomicCounters =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
||||||
|
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
|
||||||
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
||||||
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||||
@@ -66,10 +83,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||||
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
|
||||||
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
|
||||||
|
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
|
||||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||||
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
||||||
// limits they advertise still have to be legal.
|
// limits they advertise still have to be legal.
|
||||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||||
@@ -103,12 +121,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendSubpixelBits = 4;
|
constexpr GLint kFrontendSubpixelBits = 4;
|
||||||
constexpr GLint kFrontendMaxSamples = 4;
|
constexpr GLint kFrontendMaxSamples = 4;
|
||||||
|
|
||||||
|
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
||||||
|
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
||||||
|
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
|
||||||
|
// shader is allowed to compare the built-in constant against this query.
|
||||||
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
||||||
return index < 3 ? 65535 : 0;
|
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
||||||
return index < 2 ? 1024 : (index == 2 ? 64 : 0);
|
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||||
@@ -186,6 +208,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||||
}
|
}
|
||||||
|
if (bufferTarget == BufferTarget::AtomicCounter) {
|
||||||
|
// The counter family's binding count is NOT the state layer's array size: a
|
||||||
|
// counter buffer only reaches a shader as a lowered storage block, so what an
|
||||||
|
// implementation can serve is the reserved range, and that number is also what
|
||||||
|
// glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
|
||||||
|
// glBindBufferBase's all report the same ceiling.
|
||||||
|
return std::min(frontendCount,
|
||||||
|
static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||||
|
}
|
||||||
return frontendCount;
|
return frontendCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1540,15 +1572,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_LINE_WIDTH:
|
case GL_LINE_WIDTH:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
||||||
return;
|
return;
|
||||||
case GL_LAYER_PROVOKING_VERTEX:
|
|
||||||
*params = GL_LAST_VERTEX_CONVENTION;
|
|
||||||
return;
|
|
||||||
case GL_LOGIC_OP_MODE:
|
case GL_LOGIC_OP_MODE:
|
||||||
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
||||||
return;
|
return;
|
||||||
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxCombinedAtomicCounters;
|
*params = kFrontendMaxCombinedAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxCombinedAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||||
return;
|
return;
|
||||||
@@ -1564,6 +1596,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxFragmentAtomicCounters;
|
*params = kFrontendMaxFragmentAtomicCounters;
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
|
||||||
|
*params = kFrontendMaxFragmentAtomicCounterBuffers;
|
||||||
|
return;
|
||||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
||||||
return;
|
return;
|
||||||
@@ -1626,7 +1661,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
||||||
*params = 64; // TODO
|
// The same constant the map paths align to (MG_State/GLState/BufferState/
|
||||||
|
// PipeResource.h), never a literal: this number is a PROMISE about the pointers
|
||||||
|
// glMapBuffer and glMapBufferRange return, and the two used to be unrelated - the
|
||||||
|
// query said 64 while the pointers came out of a std::vector aligned to 16.
|
||||||
|
*params = static_cast<GLint>(MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_LABEL_LENGTH:
|
case GL_MAX_LABEL_LENGTH:
|
||||||
*params = 256; // TODO
|
*params = 256; // TODO
|
||||||
@@ -1672,7 +1711,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = 15; // TODO
|
*params = 15; // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_UNIFORM_LOCATIONS:
|
case GL_MAX_UNIFORM_LOCATIONS:
|
||||||
*params = 1024 * 4; // TODO
|
// The same constant the link's location allocator enforces - see ProgramObject.
|
||||||
|
*params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VARYING_COMPONENTS:
|
case GL_MAX_VARYING_COMPONENTS:
|
||||||
*params = kFrontendMaxVaryingComponents;
|
*params = kFrontendMaxVaryingComponents;
|
||||||
@@ -2002,6 +2042,24 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_BUFFER_START:
|
case GL_UNIFORM_BUFFER_START:
|
||||||
RecordIndexedOnlyGetterError(__func__, pname);
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
return;
|
return;
|
||||||
|
// glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
|
||||||
|
// is the one indexed-buffer family whose non-indexed query was never answered - so it
|
||||||
|
// fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
|
||||||
|
// stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
||||||
|
if (const auto& obj =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
|
||||||
|
*params = static_cast<GLint>(obj->GetExternalIndex());
|
||||||
|
} else {
|
||||||
|
*params = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_START:
|
||||||
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
|
return;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
|
return;
|
||||||
case GL_UNPACK_ALIGNMENT:
|
case GL_UNPACK_ALIGNMENT:
|
||||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
||||||
return;
|
return;
|
||||||
@@ -2056,9 +2114,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[3] = vp.w();
|
params[3] = vp.w();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
|
||||||
*params = GL_LAST_VERTEX_CONVENTION;
|
|
||||||
return;
|
|
||||||
case GL_MAX_ELEMENT_INDEX:
|
case GL_MAX_ELEMENT_INDEX:
|
||||||
*params = 1024 * 1024; // TODO
|
*params = 1024 * 1024; // TODO
|
||||||
return;
|
return;
|
||||||
@@ -2146,6 +2201,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_CLIP_DISTANCES:
|
case GL_MAX_CLIP_DISTANCES:
|
||||||
*params = dynamicParameters.MaxClipDistances;
|
*params = dynamicParameters.MaxClipDistances;
|
||||||
break;
|
break;
|
||||||
|
// Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
|
||||||
|
// 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
|
||||||
|
// wherever they do not actually pin a convention - claiming one is a statement about
|
||||||
|
// which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
|
||||||
|
// rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
|
||||||
|
// DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
|
||||||
|
// accepts all four values, and .provoking_vertex - which failed on both devices, in
|
||||||
|
// OPPOSITE directions - stops verifying as soon as either answer is undefined.
|
||||||
|
case GL_LAYER_PROVOKING_VERTEX:
|
||||||
|
*params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
|
||||||
|
break;
|
||||||
|
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
||||||
|
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
||||||
|
break;
|
||||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
||||||
break;
|
break;
|
||||||
@@ -2237,18 +2306,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static_cast<Uint64>(INT32_MAX)));
|
static_cast<Uint64>(INT32_MAX)));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||||
|
// NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
|
||||||
|
// clamps this family to the range a lowered counter block can actually be served
|
||||||
|
// from, which is the same number glslang compiles a layout(binding = N) atomic_uint
|
||||||
|
// against and the same one glBindBufferBase validates an index against.
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
// The conformance suite splits this evenly across every advertised binding point and
|
// The conformance suite splits this evenly across every advertised binding point and
|
||||||
// binds all of them in one glBindBuffersRange
|
// binds all of them in one glBindBuffersRange
|
||||||
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
|
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
|
||||||
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
|
// a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
|
||||||
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
// shared constant is 16384 over 8 binding points, which divides.
|
||||||
// one counter per binding point.
|
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||||
*params = std::max<GLint>(
|
|
||||||
kFrontendMaxAtomicCounterBufferSize,
|
|
||||||
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
||||||
|
// pointing into the TProgram's pool allocator. See ProgramObject::TypeFacts.
|
||||||
|
using TypeFactsRef = const MG_State::GLState::ProgramObject::TypeFacts&;
|
||||||
static GLint BoolToGLInt(bool value) {
|
static GLint BoolToGLInt(bool value) {
|
||||||
return value ? GL_TRUE : GL_FALSE;
|
return value ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -223,14 +226,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetOpaqueUniformUnitLimit(const glslang::TType* type) {
|
GLint GetOpaqueUniformUnitLimit(const TypeFactsRef type) {
|
||||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||||
if (type && type->isImage()) return dynamicParameters.MaxImageUnits;
|
if (type.isImage) return dynamicParameters.MaxImageUnits;
|
||||||
if (type && type->isTexture()) return dynamicParameters.MaxCombinedTextureImageUnits;
|
if (type.isTexture) return dynamicParameters.MaxCombinedTextureImageUnits;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ValidateOpaqueUniformUnit(const char* functionName, const glslang::TType* type, GLint unit) {
|
bool ValidateOpaqueUniformUnit(const char* functionName, const TypeFactsRef type, GLint unit) {
|
||||||
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
||||||
if (unit < 0 || unit >= limit) {
|
if (unit < 0 || unit >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -642,7 +645,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
||||||
*params = programObject->GetActiveAtomicCounterCount();
|
// Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers
|
||||||
|
// neither: the relaxed parse has already turned every atomic_uint into a plain uint
|
||||||
|
// member of a synthesized storage block by the time it builds its reflection, so it
|
||||||
|
// reports zero. The interface-query model recovers the buffers from those blocks and
|
||||||
|
// is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)
|
||||||
|
// already answers - the two queries are required to agree.
|
||||||
|
*params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER);
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_ATTRIBUTES:
|
case GL_ACTIVE_ATTRIBUTES:
|
||||||
@@ -856,10 +865,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||||
// each float back to the queried type, and it undoes the same padding itself.
|
// each float back to the queried type, and it undoes the same padding itself.
|
||||||
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
|
||||||
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
if (!ttype.isMatrix || ttype.isDouble) return false;
|
||||||
const Int columns = ttype->getMatrixCols();
|
const Int columns = ttype.matrixCols;
|
||||||
const Int rows = ttype->getMatrixRows();
|
const Int rows = ttype.matrixRows;
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
||||||
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
||||||
@@ -871,7 +880,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
||||||
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
||||||
// one and two copies of a layout rule is one too many.
|
// one and two copies of a layout rule is one too many.
|
||||||
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize) {
|
||||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,7 +913,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = (char*)programObject->MapUBO();
|
char* pUBO = (char*)programObject->MapUBO();
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
@@ -958,7 +967,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||||
auto* ttype = programObject->GetUniformTType(location);
|
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
@@ -981,10 +990,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||||
// widens back to the queried type, having lost precision at the glUniform*d that
|
// widens back to the queried type, having lost precision at the glUniform*d that
|
||||||
// stored it and not here.
|
// stored it and not here.
|
||||||
if (ttype->getBasicType() == glslang::EbtDouble) {
|
if (ttype.isDouble) {
|
||||||
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
|
||||||
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
const Int rows = ttype.isMatrix ? ttype.matrixRows
|
||||||
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
: (ttype.isVector ? ttype.vectorSize : 1);
|
||||||
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||||
// tightly packed run and never reaches the stride at all.
|
// tightly packed run and never reaches the stride at all.
|
||||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||||
@@ -1191,8 +1200,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||||
programObject.MarkUBOContentDirty();
|
programObject.MarkUBOContentDirty();
|
||||||
} else {
|
} else {
|
||||||
auto* ttype = programObject.GetUniformTType(location);
|
const auto& ttype = programObject.GetUniformTypeFacts(location);
|
||||||
if (!ttype->isTexture() && !ttype->isImage()) return;
|
if (!ttype.isTexture && !ttype.isImage) return;
|
||||||
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -2835,6 +2844,73 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface
|
||||||
|
// already carries, so this is a rename of glGetProgramResourceiv's props onto the older
|
||||||
|
// entry point's - and the two are required to agree, which is only true while both read the
|
||||||
|
// same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe
|
||||||
|
// reading its own uninitialised output.
|
||||||
|
static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) {
|
||||||
|
switch (pname) {
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
||||||
|
outProp = GL_BUFFER_BINDING;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE:
|
||||||
|
outProp = GL_BUFFER_DATA_SIZE;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS:
|
||||||
|
outProp = GL_NUM_ACTIVE_VARIABLES;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES:
|
||||||
|
outProp = GL_ACTIVE_VARIABLES;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_VERTEX_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_GEOMETRY_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_FRAGMENT_SHADER;
|
||||||
|
return true;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
outProp = GL_REFERENCED_BY_COMPUTE_SHADER;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) {
|
||||||
|
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
||||||
|
if (!programObject) return;
|
||||||
|
GLenum prop = GL_NONE;
|
||||||
|
if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname is not an active atomic counter buffer property."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vector<GLint> values;
|
||||||
|
if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"bufferIndex is not an active atomic counter buffer index."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params == nullptr) return;
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES is the only multi-value property
|
||||||
|
// here, and the caller sized its array from _ACTIVE_ATOMIC_COUNTERS.
|
||||||
|
for (SizeT i = 0; i < values.size(); ++i) params[i] = values[i];
|
||||||
|
}
|
||||||
|
|
||||||
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
||||||
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
||||||
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
|
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||||
void Uniform1d(GLint location, GLdouble v0);
|
void Uniform1d(GLint location, GLdouble v0);
|
||||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||||
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
||||||
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
||||||
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
|
||||||
|
|
||||||
enum class BlockKind {
|
enum class BlockKind {
|
||||||
Uniform, // a real GL uniform block
|
Uniform, // a real GL uniform block
|
||||||
@@ -81,19 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||||
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||||
// stage inputs/outputs, so those get it here.
|
// stage inputs/outputs, so those get it here.
|
||||||
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
if (!type.isArray || EndsWithZeroSubscript(name)) return name;
|
||||||
return name + "[0]";
|
return name + "[0]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
||||||
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
||||||
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
// `record.arraySize` is already the sized-array/reflected-size resolution; the only
|
||||||
if (type != nullptr && type->isArray()) {
|
// extra rule here is GL's 0 for a runtime-sized array.
|
||||||
if (!type->isSizedArray()) return 0;
|
GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
|
||||||
return type->getOuterArraySize();
|
if (record.type.isArray && !record.type.isSizedArray) return 0;
|
||||||
}
|
return record.arraySize;
|
||||||
return reflectedSize < 1 ? 1 : reflectedSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two spellings name the same resource when they are equal, or differ only by the
|
// Two spellings name the same resource when they are equal, or differ only by the
|
||||||
@@ -174,22 +173,21 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return static_cast<GLint>(element);
|
return static_cast<GLint>(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
|
||||||
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||||
return BlockKind::GlobalUbo;
|
return BlockKind::GlobalUbo;
|
||||||
}
|
}
|
||||||
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||||
const glslang::TType* type = block.getType();
|
if (block.type.isBuffer) return BlockKind::Storage;
|
||||||
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
|
||||||
return BlockKind::Uniform;
|
return BlockKind::Uniform;
|
||||||
}
|
}
|
||||||
|
|
||||||
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||||
// uniform matrices. 0 for a non-matrix.
|
// uniform matrices. 0 for a non-matrix.
|
||||||
GLint MatrixStrideOf(const glslang::TType* type) {
|
GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (!type.isMatrix) return 0;
|
||||||
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
|
||||||
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
|
||||||
constexpr int scalarSize = 4;
|
constexpr int scalarSize = 4;
|
||||||
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
: (strideVectorComponents == 2) ? 2 * scalarSize
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
@@ -197,9 +195,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return (vectorAlignment + 15) & ~15;
|
return (vectorAlignment + 15) & ~15;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint IsRowMajorOf(const glslang::TType* type) {
|
GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
if (!type.isMatrix) return 0;
|
||||||
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint MappedLocation(Int rawLocation) {
|
GLint MappedLocation(Int rawLocation) {
|
||||||
@@ -227,12 +225,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
||||||
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
||||||
// back to the block's own mask there would restore the over-approximation.
|
// back to the block's own mask there would restore the over-approximation.
|
||||||
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
|
||||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
Int blockCount) {
|
||||||
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||||
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
||||||
for (Int index = 0; index < uniformCount; ++index) {
|
for (Int index = 0; index < uniformCount; ++index) {
|
||||||
const auto& uniform = mutableReflection.getUniform(index);
|
const auto& uniform = reflection.uniformReflection[index];
|
||||||
const Int owner = uniform.index;
|
const Int owner = uniform.index;
|
||||||
if (owner < 0 || owner >= blockCount) continue;
|
if (owner < 0 || owner >= blockCount) continue;
|
||||||
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||||
@@ -250,7 +248,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
||||||
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
||||||
// blocks therefore keep that mask untouched.
|
// blocks therefore keep that mask untouched.
|
||||||
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||||
Int tIndex) {
|
Int tIndex) {
|
||||||
String arrayBase;
|
String arrayBase;
|
||||||
Uint element = 0;
|
Uint element = 0;
|
||||||
@@ -264,15 +262,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
|
||||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
|
||||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
blockInterfaceIndex.assign(blockCount, -1);
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||||
|
|
||||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
const auto& block = reflection.blockReflection[tIndex];
|
||||||
const BlockKind kind = ClassifyBlock(block);
|
const BlockKind kind = ClassifyBlock(block);
|
||||||
blockKind[tIndex] = kind;
|
blockKind[tIndex] = kind;
|
||||||
if (kind == BlockKind::AtomicCounter) {
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
@@ -293,7 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||||
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||||
// GetUniformBlockBinding below).
|
// GetUniformBlockBinding below).
|
||||||
const GLint declared = block.getBinding();
|
const GLint declared = block.binding;
|
||||||
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||||
@@ -315,21 +313,22 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||||
if (tIndex >= 0 && tIndex < blockCount) {
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
|
||||||
stagesFromMembers, tIndex);
|
stagesFromMembers, tIndex);
|
||||||
}
|
}
|
||||||
model.uniformBlocks.push_back(Move(resource));
|
model.uniformBlocks.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
void BuildUniformsAndBufferVariables(ProgramObject& program,
|
||||||
|
const ProgramObject::LinkArtifacts& reflection, Model& model,
|
||||||
const Vector<BlockKind>& blockKind,
|
const Vector<BlockKind>& blockKind,
|
||||||
const Vector<Int>& blockInterfaceIndex) {
|
const Vector<Int>& blockInterfaceIndex) {
|
||||||
const Uint uniformCount = program.GetUniformCount();
|
const Uint uniformCount = program.GetUniformCount();
|
||||||
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||||
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||||
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
|
||||||
const glslang::TType* type = refl.getType();
|
const auto& type = refl.type;
|
||||||
const Int owner = refl.index;
|
const Int owner = refl.index;
|
||||||
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||||
? blockKind[owner]
|
? blockKind[owner]
|
||||||
@@ -338,7 +337,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = refl.name;
|
resource.name = refl.name;
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
|
||||||
if (kind == BlockKind::Storage) {
|
if (kind == BlockKind::Storage) {
|
||||||
@@ -414,17 +413,13 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||||
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||||
// are not part of its output interface.
|
// are not part of its output interface.
|
||||||
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
|
||||||
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
|
||||||
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
|
||||||
|
|
||||||
const Int inputCount = mutableReflection.getNumPipeInputs();
|
|
||||||
for (Int index = 0; index < inputCount; ++index) {
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeInput(index);
|
const auto& refl = reflection.pipeInputReflection[index];
|
||||||
const glslang::TType* type = refl.getType();
|
const auto& type = refl.type;
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
@@ -432,10 +427,10 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||||
resource.name = WithArraySuffix(glName, type);
|
resource.name = WithArraySuffix(glName, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.location = program.GetAttributeLocation(refl.name);
|
resource.location = program.GetAttributeLocation(refl.name);
|
||||||
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
if (resource.location < 0) resource.location = MappedLocation(refl.location);
|
||||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
resource.isPerPatch = type.isPatch ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programInputs.push_back(Move(resource));
|
model.programInputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -447,16 +442,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// carries its own layout(location=N)), and a location then manufactures a color
|
// carries its own layout(location=N)), and a location then manufactures a color
|
||||||
// index of 0 where GL requires -1
|
// index of 0 where GL requires -1
|
||||||
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||||
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
const Bool lastStageIsFragment = reflection.lastStageIsFragment;
|
||||||
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const auto& refl = mutableReflection.getPipeOutput(index);
|
const auto& refl = reflection.pipeOutputReflection[index];
|
||||||
const glslang::TType* type = refl.getType();
|
const auto& type = refl.type;
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = WithArraySuffix(refl.name, type);
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(type, refl.size);
|
resource.arraySize = ArraySizeOf(refl);
|
||||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
if (resource.location < 0 || !lastStageIsFragment) {
|
if (resource.location < 0 || !lastStageIsFragment) {
|
||||||
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||||
@@ -467,11 +462,11 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||||
// layout(index = N), which the frag-data maps never saw.
|
// layout(index = N), which the frag-data maps never saw.
|
||||||
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
if (resource.locationIndex == 0 && type.hasIndex) {
|
||||||
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
resource.locationIndex = static_cast<GLint>(type.layoutIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
resource.isPerPatch = type.isPatch ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programOutputs.push_back(Move(resource));
|
model.programOutputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -511,15 +506,14 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
Model BuildModel(ProgramObject& program) {
|
Model BuildModel(ProgramObject& program) {
|
||||||
Model model;
|
Model model;
|
||||||
if (!program.GetLinkStatus()) return model;
|
if (!program.GetLinkStatus()) return model;
|
||||||
const glslang::TProgram* reflection = program.GetReflection();
|
const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
|
||||||
if (reflection == nullptr) return model;
|
|
||||||
model.valid = true;
|
model.valid = true;
|
||||||
|
|
||||||
Vector<BlockKind> blockKind;
|
Vector<BlockKind> blockKind;
|
||||||
Vector<Int> blockInterfaceIndex;
|
Vector<Int> blockInterfaceIndex;
|
||||||
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildStageIO(program, *reflection, model);
|
BuildStageIO(program, reflection, model);
|
||||||
BuildXfb(program, model);
|
BuildXfb(program, model);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -565,6 +565,75 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
queryObject->ended = true;
|
queryObject->ended = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BeginConditionalRender(GLuint id, GLenum mode) {
|
||||||
|
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
|
||||||
|
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
|
||||||
|
// treating it as its whole-framebuffer sibling is what an implementation without region
|
||||||
|
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
|
||||||
|
// obligation - see the resolve below.
|
||||||
|
Bool inverted = false;
|
||||||
|
switch (mode) {
|
||||||
|
case GL_QUERY_WAIT:
|
||||||
|
case GL_QUERY_NO_WAIT:
|
||||||
|
case GL_QUERY_BY_REGION_WAIT:
|
||||||
|
case GL_QUERY_BY_REGION_NO_WAIT:
|
||||||
|
inverted = false;
|
||||||
|
break;
|
||||||
|
case GL_QUERY_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_NO_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_BY_REGION_WAIT_INVERTED:
|
||||||
|
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
|
||||||
|
inverted = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MG_State::pGLContext->IsConditionalRenderActive()) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
const auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
// A generated NAME is not yet a query object; it becomes one at its first use with a
|
||||||
|
// target (the same rule glIsQuery answers by).
|
||||||
|
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->active) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
|
||||||
|
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
||||||
|
"Conditional rendering requires an occlusion query object.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
|
||||||
|
// render instead of stalling, so always waiting is conforming and is the only choice that
|
||||||
|
// gives the whole block one deterministic verdict. Reading it per command instead would
|
||||||
|
// let a result that lands mid-block change the answer half way through.
|
||||||
|
Uint64 samplesPassed = 0;
|
||||||
|
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
|
||||||
|
const Bool passed = samplesPassed != 0;
|
||||||
|
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndConditionalRender() {
|
||||||
|
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->EndConditionalRender();
|
||||||
|
}
|
||||||
|
|
||||||
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void QueryCounter(GLuint id, GLenum target);
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
|
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
|
||||||
|
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
|
||||||
|
// with the lock that guards it - lives in this file.
|
||||||
|
void BeginConditionalRender(GLuint id, GLenum mode);
|
||||||
|
void EndConditionalRender();
|
||||||
// Destroys every still-registered query object exactly as DeleteQueries would.
|
// Destroys every still-registered query object exactly as DeleteQueries would.
|
||||||
// GL requires queries to die with their context; called only from full library
|
// GL requires queries to die with their context; called only from full library
|
||||||
// teardown (DestroyImpl), where no context survives on any thread, so the
|
// teardown (DestroyImpl), where no context survives on any thread, so the
|
||||||
|
|||||||
@@ -661,21 +661,42 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Compressed texture formats are not supported."));
|
"Compressed texture formats are not supported."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
|
// GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
|
||||||
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
|
// in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
|
||||||
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
|
// buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
|
||||||
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
|
// MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
|
||||||
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
//
|
||||||
// above for the compressed-format path.
|
// GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in
|
||||||
|
// basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two
|
||||||
|
// fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction.
|
||||||
|
GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) {
|
||||||
|
const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat());
|
||||||
|
// A format with no known footprint has no texel count to report; answering 0 beats
|
||||||
|
// dividing by it.
|
||||||
|
if (texelByteSize == 0) return 0;
|
||||||
|
const auto* bufferTextureObject =
|
||||||
|
static_cast<const MG_State::GLState::TextureObjectBuffer*>(textureObject);
|
||||||
|
const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize;
|
||||||
|
const SizeT maxTexelCount = static_cast<SizeT>(
|
||||||
|
std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize));
|
||||||
|
return static_cast<GLint>(std::min(texelCount, maxTexelCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since
|
||||||
|
// the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This
|
||||||
|
// is what is left: a storage class with no level geometry at all. Report it instead of
|
||||||
|
// throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the
|
||||||
|
// process down, which is never an acceptable answer to a query - see the same reasoning above
|
||||||
|
// for the compressed-format path.
|
||||||
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
||||||
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
|
||||||
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
"storage class; recording GL_INVALID_OPERATION instead of terminating",
|
||||||
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", caller,
|
"MG_Impl/GLImpl", caller,
|
||||||
"Level queries are not supported for texture-buffer storage."));
|
"Level queries are not supported for this texture's storage class."));
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -3059,6 +3080,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
|
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers.
|
||||||
|
// It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query
|
||||||
|
// raised GL_INVALID_ENUM and left the caller's float untouched - which is what
|
||||||
|
// KHR-GL4x.shader_image_load_store.basic-api-texParam reads back.
|
||||||
|
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
|
||||||
|
if (params) {
|
||||||
|
*params = static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case GL_DEPTH_STENCIL_TEXTURE_MODE:
|
case GL_DEPTH_STENCIL_TEXTURE_MODE:
|
||||||
if (params) {
|
if (params) {
|
||||||
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
|
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
|
||||||
@@ -3108,6 +3138,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = GetBufferTextureTexelWidth(textureObject.get());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3123,6 +3156,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = 1; // a buffer texture is one-dimensional
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3138,6 +3174,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = 1; // a buffer texture is one-dimensional
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3207,6 +3246,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GL_TEXTURE_BUFFER_SIZE:
|
||||||
|
case GL_TEXTURE_BUFFER_OFFSET: {
|
||||||
|
// GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER
|
||||||
|
// texture addresses, so there is nothing to report for any other storage - which is
|
||||||
|
// INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with
|
||||||
|
// above.
|
||||||
|
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
||||||
|
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params) {
|
||||||
|
const auto* bufferTextureObject =
|
||||||
|
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
|
||||||
|
// Basic machine units, and UNCLAMPED - see GetBufferTextureTexelWidth for why this
|
||||||
|
// half does not take the GL_MAX_TEXTURE_BUFFER_SIZE clamp that WIDTH does.
|
||||||
|
*params = static_cast<GLint>(pname == GL_TEXTURE_BUFFER_SIZE
|
||||||
|
? bufferTextureObject->GetBufferRangeSizeInBytes()
|
||||||
|
: bufferTextureObject->GetBufferRangeOffset());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
||||||
@@ -3246,6 +3310,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3261,6 +3328,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = 1.0f; // a buffer texture is one-dimensional
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3276,6 +3346,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TextureStorageType::Buffer:
|
||||||
|
*params = 1.0f; // a buffer texture is one-dimensional
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3343,6 +3416,27 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GL_TEXTURE_BUFFER_SIZE:
|
||||||
|
case GL_TEXTURE_BUFFER_OFFSET: {
|
||||||
|
// See GetTexLevelParameteriv_State: both describe the attached buffer range of a
|
||||||
|
// GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION.
|
||||||
|
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
||||||
|
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params) {
|
||||||
|
const auto* bufferTextureObject =
|
||||||
|
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
|
||||||
|
*params = static_cast<GLfloat>(pname == GL_TEXTURE_BUFFER_SIZE
|
||||||
|
? bufferTextureObject->GetBufferRangeSizeInBytes()
|
||||||
|
: bufferTextureObject->GetBufferRangeOffset());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
||||||
@@ -3655,12 +3749,57 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
|
return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How far the region's z axis may reach. It does not mean the same thing on every target
|
||||||
|
// GL 4.6 core 18.3.2 accepts: on a CUBE MAP it selects among the six faces, which this
|
||||||
|
// frontend keeps as six separate one-slice upload targets - so the level's own extent
|
||||||
|
// says 1 and the real bound is 6. A cube-map ARRAY is one upload target whose depth
|
||||||
|
// already counts layer-faces, and a 1D array carries its layers on y (which is where GL
|
||||||
|
// puts them for this entry point too), so both are answered by the level extent.
|
||||||
|
Int GetCopyImageEndpointLayerCount(const MG_Backend::CopyImageEndpoint& endpoint,
|
||||||
|
const IntVec3& levelSize) {
|
||||||
|
if (!endpoint.IsRenderbuffer() && endpoint.Texture &&
|
||||||
|
endpoint.Texture->GetTarget() == TextureTarget::TextureCubeMap) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
return std::max(levelSize.z(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's
|
||||||
|
// boundaries. The only bounds-shaped call this validator used to make was
|
||||||
|
// ValidateCopyImageBlockAlignment, whose first line returns true for every UNCOMPRESSED
|
||||||
|
// format - so no uncompressed copy was bounded at all, and the z extent could not be
|
||||||
|
// bounded even in principle because srcZ/dstZ never reached the validator. Texture
|
||||||
|
// endpoints were covered only by accident, through the ES driver's own error, which the
|
||||||
|
// DirectGLES backend logs and swallows rather than reporting; a GL_RENDERBUFFER endpoint
|
||||||
|
// got neither (KHR-GL43.copy_image.exceeding_boundaries).
|
||||||
|
Bool ValidateCopyImageRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint, const IntVec3& levelSize,
|
||||||
|
GLint x, GLint y, GLint z, GLsizei width, GLsizei height, GLsizei depth,
|
||||||
|
const char* endpointName) {
|
||||||
|
// An extent this frontend does not know cannot bound anything, and guessing would
|
||||||
|
// reject a copy GL allows. Every caller has already established that the level
|
||||||
|
// exists and that the image is complete, so this is a belt-and-braces guard.
|
||||||
|
if (levelSize.x() <= 0 || levelSize.y() <= 0) return true;
|
||||||
|
const Int layers = GetCopyImageEndpointLayerCount(endpoint, levelSize);
|
||||||
|
if (x >= 0 && y >= 0 && z >= 0 && static_cast<Int64>(x) + width <= levelSize.x() &&
|
||||||
|
static_cast<Int64>(y) + height <= levelSize.y() && static_cast<Int64>(z) + depth <= layers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||||
|
std::format("The {} region [{}, {}, {}] + [{} x {} x {}] does not fit inside the {} x {} x {} "
|
||||||
|
"image.",
|
||||||
|
endpointName, x, y, z, width, height, depth, levelSize.x(), levelSize.y(), layers)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
|
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const MG_Backend::CopyImageEndpoint& dst,
|
const MG_Backend::CopyImageEndpoint& dst,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
if (!ValidateCopyImageObjectExists(src, "source") ||
|
if (!ValidateCopyImageObjectExists(src, "source") ||
|
||||||
!ValidateCopyImageObjectExists(dst, "destination")) {
|
!ValidateCopyImageObjectExists(dst, "destination")) {
|
||||||
@@ -3755,6 +3894,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// One region extent, measured against both images: GL 4.6 core 18.3.2 gives the copy a
|
||||||
|
// single width/height/depth and requires it to fit in the source AND the destination.
|
||||||
|
if (!ValidateCopyImageRegionBounds(src, srcLevelSize, srcX, srcY, srcZ, srcWidth, srcHeight, srcDepth,
|
||||||
|
"source") ||
|
||||||
|
!ValidateCopyImageRegionBounds(dst, dstLevelSize, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth,
|
||||||
|
"destination")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5988,8 +6135,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
};
|
};
|
||||||
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
|
const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
|
||||||
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
|
const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
|
||||||
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, dst, dstTarget,
|
if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget,
|
||||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
|
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
|
||||||
|
|||||||
@@ -514,10 +514,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
||||||
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
||||||
//
|
//
|
||||||
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
// Whether the backend can FEED it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
||||||
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
// and DirectGLES can never have it at all. What that costs is the ARRAY, not the call: GL 4.6
|
||||||
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
// core 10.3.2 defines no error for a well-formed glVertexAttribLFormat, and a GL 4.3 context
|
||||||
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
// has 64-bit attributes in core, so declining the call would be non-conformant and would make
|
||||||
|
// the four pure state queries (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET)
|
||||||
|
// unanswerable (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore
|
||||||
|
// RECORDED here and the enabled array is dropped at draw instead - loudly, once, naming the
|
||||||
|
// reason. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp; the draw-side
|
||||||
|
// drop is DirectGLES/Managers.cpp and, on DirectVulkan, VertexInputStateFactory's Float64 case.
|
||||||
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
GLuint attribindex, GLint size, GLenum type,
|
GLuint attribindex, GLint size, GLenum type,
|
||||||
GLuint relativeoffset) {
|
GLuint relativeoffset) {
|
||||||
@@ -528,14 +533,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!MG_Backend::pActiveBackendObject ||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||||
"backend has no double-precision vertex attribute support - see the "
|
"backend has no double-precision vertex attribute support - the format is recorded "
|
||||||
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
"and queryable, but the array will be DROPPED at draw and the attribute will read "
|
||||||
|
"its generic current value; see the \"64-bit vertex attributes\" / \"shaderFloat64\" "
|
||||||
|
"POST row for what that costs",
|
||||||
attribindex);
|
attribindex);
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
|
||||||
"64-bit vertex attributes are not supported by this backend."));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
|
Scenarios/SnormAttachmentScenario.cpp
|
||||||
Scenarios/PipelineFailureScenario.cpp
|
Scenarios/PipelineFailureScenario.cpp
|
||||||
Scenarios/AdvertisedLimitsScenario.cpp
|
Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
Scenarios/PixelStoreSweepScenario.cpp
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
@@ -80,8 +81,10 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||||
Scenarios/ImageTargetKindScenario.cpp
|
Scenarios/ImageTargetKindScenario.cpp
|
||||||
Scenarios/ImageFormatQualifierScenario.cpp
|
Scenarios/ImageFormatQualifierScenario.cpp
|
||||||
|
Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||||
Scenarios/SsboDeclarationFormScenario.cpp
|
Scenarios/SsboDeclarationFormScenario.cpp
|
||||||
Scenarios/Glsl420DeclarationScenario.cpp
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
|
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||||
Scenarios/BufferTextureScenario.cpp
|
Scenarios/BufferTextureScenario.cpp
|
||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.cpp
|
||||||
@@ -90,7 +93,10 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
Scenarios/CopyImageLayeredScenario.cpp
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
|
Scenarios/PackedWordReadbackScenario.cpp
|
||||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||||
|
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||||
|
Scenarios/AtomicCounterScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.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 - ATOMIC COUNTERS, END TO END.
|
||||||
|
//
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
|
||||||
|
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
|
||||||
|
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
|
||||||
|
// closing two open ends that used to be missing entirely -
|
||||||
|
//
|
||||||
|
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
|
||||||
|
// relation to the GL binding point N the application bound its buffer to (and could alias an
|
||||||
|
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
|
||||||
|
// range; and
|
||||||
|
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
|
||||||
|
// read, is re-issued as a shader-storage binding at that reserved slot.
|
||||||
|
//
|
||||||
|
// Neither end alone is observable: with only the first the shader increments a block nobody
|
||||||
|
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
|
||||||
|
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
|
||||||
|
//
|
||||||
|
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
|
||||||
|
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
|
||||||
|
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
|
||||||
|
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
|
||||||
|
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
|
||||||
|
// block is laid out the way GL says": a lowering that packed the members in declaration
|
||||||
|
// order without honouring `offset` would still pass a single-counter check.
|
||||||
|
constexpr const char* kCounterComputeSource = R"(#version 430 core
|
||||||
|
layout(local_size_x = 4) in;
|
||||||
|
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
|
||||||
|
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
|
||||||
|
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
|
||||||
|
void main() {
|
||||||
|
atomicCounterIncrement(g_first);
|
||||||
|
atomicCounterIncrement(g_second);
|
||||||
|
atomicCounterIncrement(g_second);
|
||||||
|
atomicCounterIncrement(g_other);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr int kLocalSizeX = 4;
|
||||||
|
constexpr int kWorkGroups = 2;
|
||||||
|
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
|
||||||
|
|
||||||
|
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
|
||||||
|
// that survives is also proof that the buffer's CPU-side contents reached the driver.
|
||||||
|
constexpr unsigned int kSeedFirst = 5;
|
||||||
|
constexpr unsigned int kSeedSecond = 100;
|
||||||
|
constexpr unsigned int kSeedOther = 7;
|
||||||
|
|
||||||
|
class AtomicCounterScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
GLint counters = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
||||||
|
GLint buffers = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
|
||||||
|
if (counters < 3 || buffers < 2) {
|
||||||
|
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
|
||||||
|
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
|
||||||
|
<< "; this needs 3 and 2";
|
||||||
|
}
|
||||||
|
if (!AtomicCountersAreWired()) {
|
||||||
|
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
|
||||||
|
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
|
||||||
|
<< "is still resolved from the shader-storage binding points";
|
||||||
|
}
|
||||||
|
m_program = CompileComputeProgram(kCounterComputeSource);
|
||||||
|
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
m_buffers.clear();
|
||||||
|
m_program = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
|
||||||
|
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
|
||||||
|
// frontend half (limits, reflection queries, the link-time offset rules) is
|
||||||
|
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
|
||||||
|
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
|
||||||
|
|
||||||
|
unsigned int CompileComputeProgram(const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("compute shader did not compile: ") + log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
m_buildLog = std::string("compute program did not link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
|
||||||
|
// `binding`.
|
||||||
|
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||||
|
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
|
||||||
|
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
m_buffers.push_back(buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
|
||||||
|
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||||
|
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
|
||||||
|
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dispatch() {
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glDispatchCompute(kWorkGroups, 1, 1);
|
||||||
|
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
std::string m_buildLog;
|
||||||
|
std::vector<GLuint> m_buffers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The counter values a dispatch leaves behind, per binding point and per offset within one
|
||||||
|
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
|
||||||
|
// before the wiring landed every one of these read back its seed unchanged.
|
||||||
|
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
|
||||||
|
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
|
||||||
|
|
||||||
|
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
|
||||||
|
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
|
||||||
|
|
||||||
|
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
|
||||||
|
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
|
||||||
|
<< " means the shader's increments never reached the buffer the application bound";
|
||||||
|
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
|
||||||
|
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
|
||||||
|
<< "offset was not carried through the lowering, even though offset 0 was";
|
||||||
|
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
|
||||||
|
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
|
||||||
|
<< "resolves to a different reserved slot and is where an off-by-one shows up";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second dispatch continues from where the first left off, and a re-seed between them is
|
||||||
|
// visible to the shader. Both halves of the buffer's traffic have to work, in both
|
||||||
|
// directions: the increments are only observable through the readback path, and the re-seed
|
||||||
|
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
|
||||||
|
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
||||||
|
MakeCounterBuffer(1, {0u});
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
Dispatch();
|
||||||
|
std::vector<unsigned int> values = ReadCounters(zero, 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
|
||||||
|
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
|
||||||
|
|
||||||
|
const unsigned int reseed[2] = {1000u, 2000u};
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
|
||||||
|
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), reseed);
|
||||||
|
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
|
||||||
|
|
||||||
|
Dispatch();
|
||||||
|
values = ReadCounters(zero, 2);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value did not reach the shader";
|
||||||
|
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -299,4 +299,99 @@ void main() {
|
|||||||
EXPECT_EQ(FirstGLError(), 0u);
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
|
||||||
|
// fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
|
||||||
|
// GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
|
||||||
|
// KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
|
||||||
|
// the case before any value is compared.
|
||||||
|
//
|
||||||
|
// The two halves report DIFFERENT units and only one of them is clamped, which is the thing
|
||||||
|
// easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
|
||||||
|
// BUFFER_SIZE is the range in basic machine units exactly as it was given.
|
||||||
|
TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
FirstGLError();
|
||||||
|
|
||||||
|
GLint offsetAlignment = 1;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment < 1) offsetAlignment = 1;
|
||||||
|
GLint maxTexels = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";
|
||||||
|
|
||||||
|
constexpr GLint kTexelBytes = 4; // GL_RGBA8
|
||||||
|
const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
|
||||||
|
const GLsizeiptr rangeBytes = 32 * kTexelBytes;
|
||||||
|
// Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
|
||||||
|
// than out of the texture's window would be caught.
|
||||||
|
const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;
|
||||||
|
|
||||||
|
const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||||
|
glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
|
glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";
|
||||||
|
|
||||||
|
const auto levelQuery = [](GLenum pname) {
|
||||||
|
GLint value = -1;
|
||||||
|
glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
const auto levelQueryF = [](GLenum pname) {
|
||||||
|
GLfloat value = -1.0f;
|
||||||
|
glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
|
||||||
|
<< "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
|
||||||
|
<< "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
|
||||||
|
EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
|
||||||
|
<< "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";
|
||||||
|
|
||||||
|
// The float getter is a separate switch and has drifted from the integer one before.
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";
|
||||||
|
|
||||||
|
// The whole-buffer form follows the buffer's current size instead of freezing a window.
|
||||||
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
|
||||||
|
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
// Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
|
||||||
|
// the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
|
||||||
|
GLuint plainTexture = 0;
|
||||||
|
glGenTextures(1, &plainTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, plainTexture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
GLint unused = -1;
|
||||||
|
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
|
||||||
|
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
glDeleteTextures(1, &plainTexture);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -151,6 +151,18 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_MAX_CLIP_DISTANCES is a real backend answer, not a constant: DirectGLES reports
|
||||||
|
// 0 on a driver without GL_EXT_clip_cull_distance, and DirectVulkan reports 0 without
|
||||||
|
// the shaderClipDistance device feature. On such a stack the shader above cannot
|
||||||
|
// compile - and MUST not, because declaring a clip distance the backend cannot host
|
||||||
|
// is exactly what used to link cleanly and then render nothing. Skip rather than
|
||||||
|
// fail: there is no clipping to assert about.
|
||||||
|
static bool BackendHostsTwoClipDistances() {
|
||||||
|
GLint maxClipDistances = 0;
|
||||||
|
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||||
|
return maxClipDistances >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Never assume the eight start disabled - see the header note about
|
// Never assume the eight start disabled - see the header note about
|
||||||
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
||||||
static void DisableEveryClipDistance() {
|
static void DisableEveryClipDistance() {
|
||||||
@@ -229,6 +241,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// The claim: an enabled clip distance removes the fragments where it is negative.
|
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||||
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
@@ -280,6 +295,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// draw simply failed - would pass the case above.
|
// draw simply failed - would pass the case above.
|
||||||
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
@@ -329,6 +347,9 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// passes both cases above and fails this one.
|
// passes both cases above and fails this one.
|
||||||
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
|
if (!BackendHostsTwoClipDistances()) {
|
||||||
|
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
||||||
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
|
|||||||
@@ -697,24 +697,127 @@ void main() {
|
|||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
||||||
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
||||||
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
// ones whose device lacks shaderFloat64.
|
||||||
// drawn as garbage; the matching POST row says the same thing at startup.
|
//
|
||||||
|
// What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
|
||||||
|
// a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
|
||||||
|
// context MobileGL advertises, so refusing the call would be non-conformant and would
|
||||||
|
// leave four pure state queries unanswerable
|
||||||
|
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
|
||||||
|
// queryable; the enabled array is what gets dropped, and the attribute then reads its
|
||||||
|
// generic current value. The matching POST row says exactly that at startup.
|
||||||
GLuint vao = 0;
|
GLuint vao = 0;
|
||||||
glGenVertexArrays(1, &vao);
|
glGenVertexArrays(1, &vao);
|
||||||
glBindVertexArray(vao);
|
glBindVertexArray(vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
|
|
||||||
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||||
|
<< "glVertexAttribLFormat is a legal call in a GL 4.3 context";
|
||||||
|
|
||||||
|
GLint attribSize = 0;
|
||||||
|
GLint attribType = 0;
|
||||||
|
GLint attribIsLong = 0;
|
||||||
|
GLint attribRelativeOffset = 0;
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
|
||||||
|
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
|
||||||
|
EXPECT_EQ(attribSize, 3);
|
||||||
|
EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
|
||||||
|
EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
|
||||||
|
"unconverted form; without it the state is a lie";
|
||||||
|
EXPECT_EQ(attribRelativeOffset, 8);
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glDeleteVertexArrays(1, &vao);
|
glDeleteVertexArrays(1, &vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The consequence of recording the state rather than refusing the call: a 64-bit array can
|
||||||
|
// now be ENABLED in a VAO that a draw uses, which it never could before. That must not
|
||||||
|
// take the draw down. Leaving such an array enabled with no pointer behind it is exactly
|
||||||
|
// the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
|
||||||
|
// disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
|
||||||
|
// the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
|
||||||
|
//
|
||||||
|
// The shader deliberately does NOT read location 1: that keeps the two backends on the
|
||||||
|
// same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
|
||||||
|
// by design and loudly, which is a different assertion from this one) and it is the shape
|
||||||
|
// the crash needed - an enabled array nothing set a pointer for.
|
||||||
|
TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
constexpr const char* kVs = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
constexpr const char* kFs = R"(#version 430 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
std::string error;
|
||||||
|
const unsigned int program = CompileProgram(kVs, kFs, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
ColorFbo target = MakeColorFbo(32, 32);
|
||||||
|
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
|
||||||
|
BindFbo(target);
|
||||||
|
|
||||||
|
const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
const double doubles[4] = {1.0, 2.0, 3.0, 4.0};
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
GLuint positionBuffer = 0;
|
||||||
|
GLuint doubleBuffer = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &positionBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
|
||||||
|
glGenBuffers(1, &doubleBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
|
||||||
|
glVertexAttribBinding(0, 0);
|
||||||
|
glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
|
||||||
|
glVertexAttribBinding(1, 1);
|
||||||
|
glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";
|
||||||
|
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(program);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";
|
||||||
|
|
||||||
|
const Image image = ReadPixels(target.width, target.height);
|
||||||
|
ASSERT_FALSE(image.Empty());
|
||||||
|
EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
|
||||||
|
<< "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";
|
||||||
|
|
||||||
|
glDisableVertexAttribArray(0);
|
||||||
|
glDisableVertexAttribArray(1);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &positionBuffer);
|
||||||
|
glDeleteBuffers(1, &doubleBuffer);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// Scenario - A DRAW READS imageSize() AFTER THE IMAGE TEXTURE IS RE-SPECIFIED.
|
||||||
|
//
|
||||||
|
// KHR-GL43.shader_image_size.advanced-changeSize reduced to its mechanism. The application binds
|
||||||
|
// a texture to an image unit ONCE, draws, then re-specifies that same texture with a new size
|
||||||
|
// through glTexImage2D and draws again - without touching the image unit. GL says the unit
|
||||||
|
// references the texture OBJECT, so the second draw must see the new dimensions.
|
||||||
|
//
|
||||||
|
// On Espryt it did not, and the reason is two facts meeting:
|
||||||
|
//
|
||||||
|
// 1. ES 3.1 only allows IMMUTABLE storage on an image unit, so the backend forces glTexStorage
|
||||||
|
// backing on any texture that reaches one (SyncTextureObjectToBackend's
|
||||||
|
// imageBindableStorageRequired). Immutable storage cannot be redefined, so a glTexImage2D
|
||||||
|
// that changes size or format has to MINT A NEW ES TEXTURE NAME.
|
||||||
|
// 2. The draw path never re-issued glBindImageTexture. Image units were established eagerly,
|
||||||
|
// once, when the application called glBindImageTexture, and PrepareForDraw only ever
|
||||||
|
// re-synced SAMPLED textures - so the unit kept pointing at the deleted name and
|
||||||
|
// imageSize() reported whatever that stale binding still meant.
|
||||||
|
//
|
||||||
|
// A dispatch was never affected: PrepareForCompute has always swept the image units. This is a
|
||||||
|
// draw-path scenario for exactly that reason - a compute-shaped case cannot see the defect.
|
||||||
|
//
|
||||||
|
// Both backends run it. Magma re-derives its image descriptors per draw and so was never wrong
|
||||||
|
// here, which makes it the control: the two backends have to agree on what the second draw sees.
|
||||||
|
|
||||||
|
#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 kTargetSize = 8;
|
||||||
|
|
||||||
|
constexpr const char* kVS = R"(#version 430 core
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// A single triangle that covers the whole target, with no vertex buffer at all: the
|
||||||
|
// scenario is about the image unit, so nothing else may be able to make it fail.
|
||||||
|
switch (gl_VertexID)
|
||||||
|
{
|
||||||
|
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 1: gl_Position = vec4( 3.0, -1.0, 0.0, 1.0); break;
|
||||||
|
case 2: gl_Position = vec4(-1.0, 3.0, 0.0, 1.0); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Green when the image the unit currently holds has the size the application last gave
|
||||||
|
// it, red otherwise - the conformance case's own comparison, and its own colours.
|
||||||
|
constexpr const char* kFS = R"(#version 430 core
|
||||||
|
layout(rgba8) readonly uniform image2D g_image;
|
||||||
|
uniform ivec2 g_expected_size;
|
||||||
|
layout(location = 0) out vec4 o_color;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
o_color = (imageSize(g_image) == g_expected_size) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class ImageSizeAfterRespecScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
||||||
|
if (m_color != 0) glDeleteTextures(1, &m_color);
|
||||||
|
if (m_image != 0) glDeleteTextures(1, &m_image);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_program = m_fbo = m_color = m_image = m_vao = 0;
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageSize() needs a fragment-stage image uniform; a driver that serves none should
|
||||||
|
// skip rather than fail.
|
||||||
|
bool FragmentImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
GLint maxFragmentImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits >= 1 && maxFragmentImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeProgram() {
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVS, nullptr);
|
||||||
|
glShaderSource(fs, 1, &kFS, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
glCompileShader(fs);
|
||||||
|
for (const GLuint shader : {vs, fs}) {
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "a shader did not compile: " << log;
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the program did not link: " << log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MakeRenderTarget() {
|
||||||
|
glGenTextures(1, &m_color);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_color);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTargetSize, kTargetSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
nullptr);
|
||||||
|
glGenFramebuffers(1, &m_fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw once with `expected` pushed to the shader and report the centre pixel.
|
||||||
|
void DrawAndReadCentre(int expectedWidth, int expectedHeight, unsigned char (¢re)[4]) {
|
||||||
|
const GLint location = glGetUniformLocation(m_program, "g_expected_size");
|
||||||
|
ASSERT_NE(location, -1) << "the program has no g_expected_size uniform";
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glUniform2i(location, expectedWidth, expectedHeight);
|
||||||
|
glViewport(0, 0, kTargetSize, kTargetSize);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "the draw left a GL error";
|
||||||
|
|
||||||
|
std::vector<unsigned char> pixels(static_cast<std::size_t>(kTargetSize) * kTargetSize * 4, 0);
|
||||||
|
glReadPixels(0, 0, kTargetSize, kTargetSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the target back errored";
|
||||||
|
const std::size_t offset =
|
||||||
|
(static_cast<std::size_t>(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
centre[i] = pixels[offset + static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_fbo = 0;
|
||||||
|
GLuint m_color = 0;
|
||||||
|
GLuint m_image = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The whole conformance shape: bind once, draw, re-specify the SAME texture smaller, draw
|
||||||
|
// again. The first draw is the control - it proves the binding and the shader work at all -
|
||||||
|
// and the second is the regression pin. Blue would mean the draw never ran; red means the
|
||||||
|
// image unit answered with the size the texture had BEFORE the re-spec.
|
||||||
|
TEST_F(ImageSizeAfterRespecScenario, ADrawSeesTheNewSizeOfARespecifiedImageTexture) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!FragmentImagesAreUsable()) GTEST_SKIP() << "no fragment-stage image uniform available";
|
||||||
|
|
||||||
|
m_program = MakeProgram();
|
||||||
|
if (m_program == 0) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
MakeRenderTarget();
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "setting the render target up errored";
|
||||||
|
|
||||||
|
glGenTextures(1, &m_image);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_image);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glBindImageTexture(0, m_image, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "binding the image texture errored";
|
||||||
|
|
||||||
|
unsigned char centre[4] = {0, 0, 0, 0};
|
||||||
|
DrawAndReadCentre(32, 32, centre);
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got ("
|
||||||
|
<< static_cast<int>(centre[0]) << ", "
|
||||||
|
<< static_cast<int>(centre[1]) << ", "
|
||||||
|
<< static_cast<int>(centre[2]) << ")";
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
||||||
|
|
||||||
|
// The re-spec. The image unit is deliberately NOT re-bound: GL 4.6 core 8.26 says the
|
||||||
|
// unit references the texture object, so this alone has to be visible to the next draw.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_image);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the image texture errored";
|
||||||
|
|
||||||
|
DrawAndReadCentre(16, 16, centre);
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[0]), 0)
|
||||||
|
<< "after the re-spec the draw still sees the OLD image size; centre pixel was ("
|
||||||
|
<< static_cast<int>(centre[0]) << ", " << static_cast<int>(centre[1]) << ", "
|
||||||
|
<< static_cast<int>(centre[2]) << ")";
|
||||||
|
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IoBlockNameCollisionScenario.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 - ONE BLOCK NAME USED IN BOTH DIRECTIONS BY ONE STAGE STILL CARRIES ITS PAYLOAD.
|
||||||
|
//
|
||||||
|
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface blocks, so a
|
||||||
|
// single stage may legally write
|
||||||
|
//
|
||||||
|
// in TcsData { ... } tes_in[];
|
||||||
|
// out TcsData { ... } tes_out;
|
||||||
|
//
|
||||||
|
// The tessellation evaluation stage of both interface-block tests in
|
||||||
|
// KHR-GL42/43.shading_language_420pack does exactly that, and MobileGL's backend used to
|
||||||
|
// hand the shape straight through: SPIRV-Cross splits the namespace the same way glslang
|
||||||
|
// does (block_input_names vs block_output_names) and re-emits BOTH blocks under the name
|
||||||
|
// TcsData, so the generated ESSL declares two different blocks of one name in one shader.
|
||||||
|
// Adreno's ES compiler keeps them apart. Mali's does not - the stage compiles, the program
|
||||||
|
// links, and the evaluation stage's writes never reach the geometry stage, which is all 22
|
||||||
|
// of that group's Mali failures and none of Adreno's or DirectVulkan's.
|
||||||
|
//
|
||||||
|
// Both cases below drive the SAME five-stage pipeline (vertex -> tessellation control ->
|
||||||
|
// tessellation evaluation -> geometry -> fragment) and differ only in whether the
|
||||||
|
// evaluation stage reuses one name. The distinct-name case is the negative control: it is
|
||||||
|
// what says a red pixel in the colliding case is about the name and not about this machine's
|
||||||
|
// tessellation, its geometry stage, or the block mechanism in general.
|
||||||
|
//
|
||||||
|
// Colour code, so a failure names its own cause:
|
||||||
|
// green - the payload crossed all four stage boundaries, which is the pass.
|
||||||
|
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
||||||
|
// backend program was rejected and every draw became a no-op).
|
||||||
|
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
||||||
|
// failure is not about interface blocks.
|
||||||
|
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
||||||
|
// zeroed or garbage. That is the defect this scenario exists for.
|
||||||
|
//
|
||||||
|
// llvmpipe and lavapipe run this faithfully but do NOT reproduce the original defect - the
|
||||||
|
// aliasing is a Mali ES compiler behaviour. Read a green run here as "the rename did not
|
||||||
|
// break the ordinary path"; the claim it pins on the device is the CTS group above.
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// The payload starts here and is copied, unmodified, through every block below.
|
||||||
|
const char* const kVertexSource = R"(#version 420 core
|
||||||
|
out VsData {
|
||||||
|
vec4 payload;
|
||||||
|
} vs_out;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kTessControlSource = R"(#version 420 core
|
||||||
|
layout(vertices = 1) out;
|
||||||
|
in VsData {
|
||||||
|
vec4 payload;
|
||||||
|
} tcs_in[];
|
||||||
|
out TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
} tcs_out[];
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelOuter[3] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
gl_TessLevelInner[1] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// THE CASE UNDER TEST: one name, both directions, in one stage.
|
||||||
|
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
} tes_in[];
|
||||||
|
out TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
} tes_out;
|
||||||
|
out float tes_gs_alive;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_out.payload = tes_in[0].payload;
|
||||||
|
tes_gs_alive = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The negative control: byte-identical but for the output block's name.
|
||||||
|
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
} tes_in[];
|
||||||
|
out TesData {
|
||||||
|
vec4 payload;
|
||||||
|
} tes_out;
|
||||||
|
out float tes_gs_alive;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_out.payload = tes_in[0].payload;
|
||||||
|
tes_gs_alive = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// One geometry source per evaluation stage, because the block it consumes is named
|
||||||
|
// after the block the evaluation stage produced.
|
||||||
|
const char* const kCollidingGeometrySource = R"(#version 420 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
in TcsData {
|
||||||
|
vec4 payload;
|
||||||
|
} gs_in[];
|
||||||
|
in float tes_gs_alive[];
|
||||||
|
out GsData {
|
||||||
|
vec4 payload;
|
||||||
|
} gs_out;
|
||||||
|
out float gs_fs_alive;
|
||||||
|
void EmitCorner(vec2 corner)
|
||||||
|
{
|
||||||
|
gs_out.payload = gs_in[0].payload;
|
||||||
|
gs_fs_alive = tes_gs_alive[0];
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
EmitCorner(vec2(-1.0, -1.0));
|
||||||
|
EmitCorner(vec2(-1.0, 1.0));
|
||||||
|
EmitCorner(vec2( 1.0, -1.0));
|
||||||
|
EmitCorner(vec2( 1.0, 1.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* const kDistinctGeometrySource = R"(#version 420 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(triangle_strip, max_vertices = 4) out;
|
||||||
|
in TesData {
|
||||||
|
vec4 payload;
|
||||||
|
} gs_in[];
|
||||||
|
in float tes_gs_alive[];
|
||||||
|
out GsData {
|
||||||
|
vec4 payload;
|
||||||
|
} gs_out;
|
||||||
|
out float gs_fs_alive;
|
||||||
|
void EmitCorner(vec2 corner)
|
||||||
|
{
|
||||||
|
gs_out.payload = gs_in[0].payload;
|
||||||
|
gs_fs_alive = tes_gs_alive[0];
|
||||||
|
gl_Position = vec4(corner, 0.0, 1.0);
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
EmitCorner(vec2(-1.0, -1.0));
|
||||||
|
EmitCorner(vec2(-1.0, 1.0));
|
||||||
|
EmitCorner(vec2( 1.0, -1.0));
|
||||||
|
EmitCorner(vec2( 1.0, 1.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Red when the PLAIN varying did not arrive, so "the pipeline is broken" and "the
|
||||||
|
// block payload is broken" cannot be confused for one another.
|
||||||
|
const char* const kFragmentSource = R"(#version 420 core
|
||||||
|
in GsData {
|
||||||
|
vec4 payload;
|
||||||
|
} fs_in;
|
||||||
|
in float gs_fs_alive;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = gs_fs_alive > 0.5 ? fs_in.payload : vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class IoBlockNameCollisionScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
if (!BackendHostsTessellationAndGeometry()) {
|
||||||
|
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
||||||
|
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
||||||
|
<< "carry a block through";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
for (const GLuint program : m_programs) {
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
m_programs.clear();
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_vao = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_MAX_TESS_GEN_LEVEL is a real backend answer, not a frontend constant: it
|
||||||
|
// reads 0 on a DirectGLES driver without GL_EXT_tessellation_shader and on a
|
||||||
|
// DirectVulkan device without the tessellationShader feature. There is no
|
||||||
|
// five-stage pipeline to assert about on such a stack.
|
||||||
|
static bool BackendHostsTessellationAndGeometry() {
|
||||||
|
GLint maxTessGenLevel = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
|
GLint maxGeometryOutputVertices = 0;
|
||||||
|
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
||||||
|
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
||||||
|
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
||||||
|
GL_FRAGMENT_SHADER};
|
||||||
|
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
||||||
|
geometrySource, kFragmentSource};
|
||||||
|
|
||||||
|
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
shaders[i] = glCreateShader(stages[i]);
|
||||||
|
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
||||||
|
glCompileShader(shaders[i]);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (!compiled) {
|
||||||
|
m_buildLog = InfoLog(shaders[i], true);
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
if (shader != 0) glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
glDeleteShader(shader);
|
||||||
|
}
|
||||||
|
if (!linked) {
|
||||||
|
m_buildLog = InfoLog(program, false);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
m_programs.push_back(program);
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
||||||
|
// than something that could be mistaken for a zeroed payload.
|
||||||
|
Rgba8 DrawAndReadCentre(GLuint program) const {
|
||||||
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||||
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(program);
|
||||||
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
||||||
|
glDrawArrays(GL_PATCHES, 0, 1);
|
||||||
|
|
||||||
|
Rgba8 pixel{};
|
||||||
|
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||||
|
return pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsGreen(const Rgba8& pixel) {
|
||||||
|
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& BuildLog() const { return m_buildLog; }
|
||||||
|
|
||||||
|
static GLenum FirstGLError() {
|
||||||
|
const GLenum first = glGetError();
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
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> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
||||||
|
if (isShader) {
|
||||||
|
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
} else {
|
||||||
|
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||||
|
}
|
||||||
|
return std::string(log.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::string m_buildLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The negative control, and it runs first on purpose: if this one is not green there
|
||||||
|
// is nothing to conclude from the case below it.
|
||||||
|
//
|
||||||
|
// It is also the CALIBRATION. GL_MAX_TESS_GEN_LEVEL answers for the tessellation
|
||||||
|
// stages honestly, but nothing MobileGL reports answers for the geometry stage the
|
||||||
|
// same way (GL_MAX_GEOMETRY_* are frontend constants and an ES driver may legitimately
|
||||||
|
// report zero geometry storage blocks while having geometry shaders), so a stack that
|
||||||
|
// cannot build a five-stage program at all is recognised here, by trying.
|
||||||
|
TEST_F(IoBlockNameCollisionScenario, DistinctlyNamedBlocksCarryThePayloadThroughFiveStages) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
||||||
|
if (program == 0) {
|
||||||
|
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||||
|
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||||
|
<< BuildLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndReadCentre(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(IsGreen(centre)) << "the control pipeline did not deliver its payload: " << centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(IoBlockNameCollisionScenario, OneBlockNameInBothDirectionsStillCarriesThePayload) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
// Same calibration as the case above, and for the same reason: a five-stage program
|
||||||
|
// this stack cannot build at all is not evidence about block names. Only once the
|
||||||
|
// DISTINCT-name build succeeds does a failure of the colliding one mean something.
|
||||||
|
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
||||||
|
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
||||||
|
<< Gl().BackendName() << ", so there is no block to carry through: "
|
||||||
|
<< BuildLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legal desktop GLSL: input and output block names live in separate namespaces, so
|
||||||
|
// the evaluation stage below declares TcsData twice and must still compile. The
|
||||||
|
// control above having built is what makes this assertion about the NAME.
|
||||||
|
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
||||||
|
ASSERT_NE(program, 0u)
|
||||||
|
<< "an interface block name reused across the two directions of one stage is legal "
|
||||||
|
"desktop GLSL, but the program did not build: "
|
||||||
|
<< BuildLog();
|
||||||
|
|
||||||
|
const Rgba8 centre = DrawAndReadCentre(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
EXPECT_TRUE(IsGreen(centre))
|
||||||
|
<< "the payload did not survive the stage that names its input and output block "
|
||||||
|
"the same: "
|
||||||
|
<< centre << " (blue: nothing drew; red: the plain varying was lost too; black: "
|
||||||
|
"the block arrived empty)";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.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 - READING EVERY LAYER OF A 1D-ARRAY / CUBE-MAP-ARRAY LEVEL BACK.
|
||||||
|
//
|
||||||
|
// glGetTexImage has no ES equivalent, so Espryt serves it by attaching the level to a scratch
|
||||||
|
// READ framebuffer and reading it with glReadPixels. Two of the targets it has to answer for do
|
||||||
|
// not fit that shape the way the others do, and both came back as zeroes in
|
||||||
|
// KHR-GL4x.shader_image_load_store.basic-allTargets-* and .non-layered_binding:
|
||||||
|
//
|
||||||
|
// * GL_TEXTURE_1D_ARRAY carries its LAYERS in the state-side height - that is what
|
||||||
|
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means - while the ES texture behind it is a 2D
|
||||||
|
// array of height 1 with the layers in depth. The readback used the state-side shape, so it
|
||||||
|
// asked layer 0 for a `layers`-row rectangle that layer does not have: row 0 was the only one
|
||||||
|
// that could be right, and everything past it was whatever reading outside an attachment
|
||||||
|
// produces.
|
||||||
|
// * GL_TEXTURE_CUBE_MAP_ARRAY has no glFramebufferTexture2D target token at all, so the 2D
|
||||||
|
// attach it used to take errored, the scratch FBO stayed incomplete, and every read fell
|
||||||
|
// through to the CPU shadow - which holds what was UPLOADED, i.e. the seed, not what the
|
||||||
|
// shader stored.
|
||||||
|
//
|
||||||
|
// Both cases store from a compute dispatch (so the only copy of the data is the GPU one and a
|
||||||
|
// stale shadow cannot pass) and then read the whole level back in one glGetTexImage, checking
|
||||||
|
// every layer separately so a failure names which one. r32ui throughout: it is a core GLSL ES
|
||||||
|
// image format, so nothing here can be confused with the missing-format story that
|
||||||
|
// ImageFormatQualifierScenario covers.
|
||||||
|
//
|
||||||
|
// Magma reads these back through its own path and is unaffected by the ES attachment rules, so
|
||||||
|
// both cases run on both backends and must agree.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#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;
|
||||||
|
constexpr int kArrayLayers = 3; // enough that "layer 0 only" is visibly wrong
|
||||||
|
constexpr int kCubeLayerFaces = 12; // two cubes, which is what the conformance case uses
|
||||||
|
// A value no store writes, so "the store never landed" and "the store wrote the wrong
|
||||||
|
// thing" cannot be confused - and so a readback served from the stale CPU shadow is
|
||||||
|
// recognisable on sight.
|
||||||
|
constexpr GLuint kSeed = 0xFEEDBEEFu;
|
||||||
|
// Deliberately not 0: the unit has to travel through glUniform1i and be baked into the
|
||||||
|
// generated ESSL, so a defect there cannot hide behind the default.
|
||||||
|
constexpr GLint kImageUnit = 1;
|
||||||
|
|
||||||
|
GLuint Expected1DArrayTexel(int x, int layer) {
|
||||||
|
return 1000u + static_cast<GLuint>(layer) * 100u + static_cast<GLuint>(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint ExpectedCubeArrayTexel(int x, int y, int layerFace) {
|
||||||
|
return 1000u + static_cast<GLuint>(layerFace) * 100u + static_cast<GLuint>(y) * 10u +
|
||||||
|
static_cast<GLuint>(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One invocation per texel, and the value it writes is a function of its coordinate - so
|
||||||
|
// a layer read from the wrong slice does not merely differ, it says which slice it came
|
||||||
|
// from.
|
||||||
|
const char* k1DArrayStoreSource = R"(#version 430 core
|
||||||
|
|
||||||
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
|
||||||
|
layout (r32ui) writeonly uniform uimage1DArray uni_image;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
uint x = gl_GlobalInvocationID.x;
|
||||||
|
uint layer = gl_GlobalInvocationID.z;
|
||||||
|
imageStore(uni_image, ivec2(int(x), int(layer)), uvec4(1000u + layer * 100u + x, 0u, 0u, 0u));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kCubeArrayStoreSource = R"(#version 430 core
|
||||||
|
|
||||||
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||||
|
|
||||||
|
layout (r32ui) writeonly uniform uimageCubeArray uni_image;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
uint x = gl_GlobalInvocationID.x;
|
||||||
|
uint y = gl_GlobalInvocationID.y;
|
||||||
|
uint layerFace = gl_GlobalInvocationID.z;
|
||||||
|
imageStore(uni_image, ivec3(int(x), int(y), int(layerFace)),
|
||||||
|
uvec4(1000u + layerFace * 100u + y * 10u + x, 0u, 0u, 0u));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class LayeredTextureReadbackScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glUseProgram(0);
|
||||||
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||||
|
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
||||||
|
m_programs.clear();
|
||||||
|
m_textures.clear();
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
||||||
|
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
||||||
|
}
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImagesAreUsable() const {
|
||||||
|
GLint maxImageUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||||
|
GLint maxComputeImageUniforms = 0;
|
||||||
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||||
|
while (glGetError() != GL_NO_ERROR) {
|
||||||
|
}
|
||||||
|
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint MakeComputeProgram(const char* source) {
|
||||||
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
m_programs.push_back(program);
|
||||||
|
glAttachShader(program, shader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[4096] = {};
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
ADD_FAILURE() << "the compute program did not link: " << log;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint TrackTexture() {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
m_textures.push_back(texture);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// layered = GL_TRUE, i.e. the whole level: that is what makes every layer reachable
|
||||||
|
// from one dispatch, and it is what glBindImageTextures is specified to pass.
|
||||||
|
bool DispatchStore(GLuint program, GLuint texture, GLsizei groupsX, GLsizei groupsY, GLsizei groupsZ) {
|
||||||
|
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_R32UI);
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << "glBindImageTexture errored with " << GLErrorName(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
glUseProgram(program);
|
||||||
|
const GLint location = glGetUniformLocation(program, "uni_image");
|
||||||
|
if (location < 0) {
|
||||||
|
ADD_FAILURE() << "the image uniform was not reflected";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
glUniform1i(location, kImageUnit);
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << "assigning the image unit errored with " << GLErrorName(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
glDispatchCompute(groupsX, groupsY, groupsZ);
|
||||||
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||||
|
glUseProgram(0);
|
||||||
|
if (const GLenum error = FirstGLError()) {
|
||||||
|
ADD_FAILURE() << "the dispatch errored with " << GLErrorName(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GLuint> m_programs;
|
||||||
|
std::vector<GLuint> m_textures;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The 1D-array half. A layer past the first is the whole test: layer 0 lines up with the
|
||||||
|
// ES image's only row whichever way the axes are read, so a readback that never swapped
|
||||||
|
// them still got it right and only the deeper layers came back wrong.
|
||||||
|
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerOfA1DArray) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(k1DArrayStoreSource);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
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<GLuint> seed(static_cast<std::size_t>(kExtent) * kArrayLayers, kSeed);
|
||||||
|
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R32UI, kExtent, kArrayLayers, 0, GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||||
|
seed.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI 1D-array texture errored";
|
||||||
|
|
||||||
|
if (!DispatchStore(program, texture, kExtent, 1, kArrayLayers)) return;
|
||||||
|
|
||||||
|
std::vector<GLuint> texels(seed.size(), 0u);
|
||||||
|
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
|
||||||
|
|
||||||
|
// GL hands a 1D array back as a plain two-dimensional image whose ROWS are the
|
||||||
|
// layers, so the destination index is layer * width + x.
|
||||||
|
for (int layer = 0; layer < kArrayLayers; ++layer) {
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
const std::size_t index = static_cast<std::size_t>(layer) * kExtent + x;
|
||||||
|
EXPECT_EQ(texels[index], Expected1DArrayTexel(x, layer))
|
||||||
|
<< "layer " << layer << " texel " << x << " read back "
|
||||||
|
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback came "
|
||||||
|
"from the stale CPU shadow)"
|
||||||
|
: "an unexpected value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cube-map-array half. glFramebufferTexture2D has no token for the target, so the
|
||||||
|
// scratch FBO used to stay incomplete and every read - including layer 0 - was answered
|
||||||
|
// from the CPU shadow; the seed is what makes that visible rather than merely wrong.
|
||||||
|
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerFaceOfACubeMapArray) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
||||||
|
|
||||||
|
const GLuint program = MakeComputeProgram(kCubeArrayStoreSource);
|
||||||
|
if (program == 0) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces, kSeed);
|
||||||
|
glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_R32UI, kExtent, kExtent, kCubeLayerFaces, 0, GL_RED_INTEGER,
|
||||||
|
GL_UNSIGNED_INT, seed.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI cube-map-array texture errored";
|
||||||
|
|
||||||
|
if (!DispatchStore(program, texture, kExtent, kExtent, kCubeLayerFaces)) return;
|
||||||
|
|
||||||
|
std::vector<GLuint> texels(seed.size(), 0u);
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
||||||
|
|
||||||
|
for (int layerFace = 0; layerFace < kCubeLayerFaces; ++layerFace) {
|
||||||
|
for (int y = 0; y < kExtent; ++y) {
|
||||||
|
for (int x = 0; x < kExtent; ++x) {
|
||||||
|
const std::size_t index =
|
||||||
|
(static_cast<std::size_t>(layerFace) * kExtent + y) * kExtent + x;
|
||||||
|
EXPECT_EQ(texels[index], ExpectedCubeArrayTexel(x, y, layerFace))
|
||||||
|
<< "layer-face " << layerFace << " texel (" << x << ", " << y << ") read back "
|
||||||
|
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback "
|
||||||
|
"came from the stale CPU shadow)"
|
||||||
|
: "an unexpected value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackedWordReadbackScenario.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
|
||||||
|
//
|
||||||
|
// glGetTexImage of a 32-bit packed format read with its OWN client type owes the application the
|
||||||
|
// words the image HOLDS, and KHR-GL43.copy_image compares exactly those words. Two routes used to
|
||||||
|
// answer, and both are wrong for a level glCopyImageSubData wrote:
|
||||||
|
//
|
||||||
|
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
||||||
|
// RGB9_E5 shared exponent and collapses an R11F_G11F_B10F NaN payload to 1;
|
||||||
|
// * the CPU shadow only holds what was UPLOADED, and the mirror that replays a copy into it
|
||||||
|
// declines - silently - for a renderbuffer source, which has no shadow to mirror from.
|
||||||
|
//
|
||||||
|
// Both are pinned here with words the CTS itself uses, because both failures are invisible to a
|
||||||
|
// value comparison: every assertion below is on BITS that decode to the very value the wrong
|
||||||
|
// answer also decodes to.
|
||||||
|
//
|
||||||
|
// The fix is a raw-word route (DirectGLES::ReadPackedLevelWordsViaScratch: copy the level into a
|
||||||
|
// scratch GL_R32UI image, read that back as unsigned integers), and DirectVulkan reaches the same
|
||||||
|
// place through PackReadbackToClientOrPbo's raw-word branch over the staging bytes - so these
|
||||||
|
// scenarios are backend-agnostic on purpose.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <ios>
|
||||||
|
#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 kExtent = 4;
|
||||||
|
|
||||||
|
// The non-canonical RGB9_E5 word KHR-GL43.copy_image writes: R=0, G=0, B mantissa 63,
|
||||||
|
// shared exponent 31, i.e. the value 8064, which the spec's own encoder would emit as
|
||||||
|
// 0xe7e00000 instead. Anything that decodes and re-encodes hands back the canonical word.
|
||||||
|
//
|
||||||
|
// Reinterpreted in the destination of an RGB9_E5 -> R11F_G11F_B10F copy it is R=0,
|
||||||
|
// G=1920, B=995 - and B's 5-bit exponent is all ones with a nonzero mantissa, i.e. a NaN
|
||||||
|
// whose payload 3 does not survive a float32 round trip (it comes back as the canonical
|
||||||
|
// payload 1, B=993, word 0xf87c0000). The two defects therefore land on the same word.
|
||||||
|
constexpr GLuint kRgb9E5Word = 0xf8fc0000u;
|
||||||
|
|
||||||
|
// The R11F_G11F_B10F word the same test pairs with it: R=0, G=0, B = exponent 12,
|
||||||
|
// mantissa 0 = 0.125. As an RGB9_E5 word it is all-zero channels with a shared exponent of
|
||||||
|
// 12, which the canonical encoder would write as 0x00000000 - so a decode/re-encode of THIS
|
||||||
|
// one loses every bit that distinguishes it.
|
||||||
|
constexpr GLuint kR11fG11fB10fWord = 0x60000000u;
|
||||||
|
|
||||||
|
class PackedWordReadbackScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
DrainErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DeleteObjects();
|
||||||
|
DrainErrors();
|
||||||
|
ScenarioTest::TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DrainErrors() {
|
||||||
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteObjects() {
|
||||||
|
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||||
|
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||||
|
if (m_rbo != 0) glDeleteRenderbuffers(1, &m_rbo);
|
||||||
|
m_src = 0;
|
||||||
|
m_dst = 0;
|
||||||
|
m_rbo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A complete single-level texture whose every texel holds `word`, uploaded through the
|
||||||
|
// packed client type so the stored bits are the client's bits and nothing has had a
|
||||||
|
// chance to re-encode them.
|
||||||
|
GLuint MakePackedTexture(GLenum internalFormat, GLenum type, GLuint word) {
|
||||||
|
const std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, word);
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), kExtent, kExtent, 0, GL_RGB, type,
|
||||||
|
words.data());
|
||||||
|
// What Utils::makeTextureComplete does in the conformance cases, and what
|
||||||
|
// glCopyImageSubData requires of both endpoints.
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every texel of level 0, as raw client words.
|
||||||
|
std::vector<GLuint> ReadPackedWords(GLuint texture, GLenum type) {
|
||||||
|
std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, 0xDEADBEEFu);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, type, words.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The copy under test. Returns the error it raised so a driver that cannot perform the
|
||||||
|
// move at all can skip rather than fail: the point of these cases is which BITS come
|
||||||
|
// back, and there are none to compare if the copy never happened.
|
||||||
|
GLenum CopyWholeImage(GLuint srcName, GLenum srcTarget, GLuint dstName, GLenum dstTarget) {
|
||||||
|
DrainErrors();
|
||||||
|
glCopyImageSubData(srcName, srcTarget, 0, 0, 0, 0, dstName, dstTarget, 0, 0, 0, 0, kExtent, kExtent,
|
||||||
|
1);
|
||||||
|
const GLenum error = glGetError();
|
||||||
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the copy recorded more than one error";
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExpectEveryTexel(const std::vector<GLuint>& words, GLuint expected, const char* what) {
|
||||||
|
for (std::size_t i = 0; i < words.size(); ++i) {
|
||||||
|
ASSERT_EQ(words[i], expected)
|
||||||
|
<< what << ": texel " << i << " read 0x" << std::hex << words[i] << ", expected 0x"
|
||||||
|
<< expected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_src = 0;
|
||||||
|
GLuint m_dst = 0;
|
||||||
|
GLuint m_rbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The control that has to hold before either regression means anything: a packed word
|
||||||
|
// uploaded and read straight back must be the SAME word, not merely the same colour.
|
||||||
|
TEST_F(PackedWordReadbackScenario, AnUploadedPackedWordReadsBackVerbatim) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "RGB9_E5 upload";
|
||||||
|
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word, "RGB9_E5 round trip");
|
||||||
|
|
||||||
|
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "R11F_G11F_B10F upload";
|
||||||
|
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kR11fG11fB10fWord,
|
||||||
|
"R11F_G11F_B10F round trip");
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.copy_image.functional rgb9_e5 -> r11f_g11f_b10f, all nine target combinations of
|
||||||
|
// which failed on both GPUs. glCopyImageSubData is a raw block move, so the destination
|
||||||
|
// physically holds the source's word - but the readback decoded it to float and re-encoded,
|
||||||
|
// and the destination's blue field is a NaN whose payload float32 does not carry. Every
|
||||||
|
// texel came back 0xf87c0000 (payload 1) instead of 0xf8fc0000 (payload 3): the same
|
||||||
|
// "colour", two bits apart.
|
||||||
|
TEST_F(PackedWordReadbackScenario, ACopiedRgb9E5WordSurvivesInAnR11fG11fB10fDestination) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
||||||
|
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, 0u);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup";
|
||||||
|
|
||||||
|
const GLenum copyError = CopyWholeImage(m_src, GL_TEXTURE_2D, m_dst, GL_TEXTURE_2D);
|
||||||
|
if (copyError != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||||
|
GTEST_SKIP() << "this driver declined the RGB9_E5 -> R11F_G11F_B10F copy (" << copyError << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kRgb9E5Word,
|
||||||
|
"copied word in the R11F_G11F_B10F destination");
|
||||||
|
// ...and the source is still the source. This is verify()'s FIRST check in the
|
||||||
|
// conformance case, and the half that a canonicalizing readback fails on its own.
|
||||||
|
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word,
|
||||||
|
"the RGB9_E5 source after the copy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.copy_image.functional *->rgb9_e5 with a GL_RENDERBUFFER source: exactly the three
|
||||||
|
// renderbuffer combinations of each such family failed, and no texture one did. The
|
||||||
|
// destination's CPU shadow is what the readback answered from, the mirror that replays a
|
||||||
|
// copy into it declines when an endpoint is a renderbuffer (there is no shadow to mirror
|
||||||
|
// FROM), and the decline is silent - so glGetTexImage handed back the destination's
|
||||||
|
// pre-copy contents. The word chosen here makes that unmissable: it decodes to the same
|
||||||
|
// all-zero channels the canonical encoder would write as 0x00000000.
|
||||||
|
TEST_F(PackedWordReadbackScenario, ACopyThroughARenderbufferReachesAnRgb9E5Destination) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
m_src = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
||||||
|
m_dst = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, 0xFFFFFFFFu);
|
||||||
|
glGenRenderbuffers(1, &m_rbo);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, m_rbo);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_R11F_G11F_B10F, kExtent, kExtent);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "renderbuffer setup";
|
||||||
|
|
||||||
|
// The conformance case's own shape: texture -> renderbuffer -> texture.
|
||||||
|
const GLenum toRenderbuffer = CopyWholeImage(m_src, GL_TEXTURE_2D, m_rbo, GL_RENDERBUFFER);
|
||||||
|
if (toRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||||
|
GTEST_SKIP() << "this driver declined a renderbuffer copy destination (" << toRenderbuffer << ")";
|
||||||
|
}
|
||||||
|
const GLenum fromRenderbuffer = CopyWholeImage(m_rbo, GL_RENDERBUFFER, m_dst, GL_TEXTURE_2D);
|
||||||
|
if (fromRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||||
|
GTEST_SKIP() << "this driver declined a renderbuffer copy source (" << fromRenderbuffer << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_5_9_9_9_REV), kR11fG11fB10fWord,
|
||||||
|
"copied word in the RGB9_E5 destination");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SnormAttachmentScenario.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 - SIGNED-NORMALIZED COLOUR ATTACHMENTS, on a live driver.
|
||||||
|
//
|
||||||
|
// The bug: a GLES driver without GL_EXT_render_snorm treats every signed-normalized format as
|
||||||
|
// texture-only. DirectGLES had a colour-renderable substitute for exactly one of the eight
|
||||||
|
// (GL_RGB16_SNORM, through the three-channel widening), so an R8_SNORM or R16_SNORM attachment got
|
||||||
|
// no storage the driver would render into: the ES framebuffer was incomplete, the draw landed
|
||||||
|
// nowhere, and glGetTexImage fell through to the CPU shadow - all zeroes for a texture created with
|
||||||
|
// no data. KHR-GL4x.texture_swizzle renders into a SINGLE-CHANNEL SNORM output for every one of its
|
||||||
|
// SNORM source formats, which is why all 46 of its GL43 SNORM cases failed on Mali.
|
||||||
|
//
|
||||||
|
// THE OTHER HALF, and the reason this scenario asserts VALUES rather than only completeness: the
|
||||||
|
// substitute has to be exact. A half float's 11-bit mantissa cannot represent a 16-bit SNORM
|
||||||
|
// channel - 23451/32767 quantizes about six SNORM steps away, against a conformance window of one -
|
||||||
|
// so the 16-bit formats must land on a 32-bit float even though the 8-bit ones are fine in a half.
|
||||||
|
// Trading 46 visible failures for silent precision loss in Iris' SNORM normal buffers would be the
|
||||||
|
// worse outcome, so the round trip below is pinned tightly enough to fail on a half-float substitute
|
||||||
|
// (tolerance two SNORM steps, half-float error six).
|
||||||
|
//
|
||||||
|
// WHAT THIS GATE CAN AND CANNOT SEE. Both CI drivers (Mesa llvmpipe) and Adreno expose
|
||||||
|
// GL_EXT_render_snorm, so they take the NATIVE path here and the substitution stays dead. That is
|
||||||
|
// precisely why the assertions are written as invariants of the format rather than of the fallback:
|
||||||
|
// "a signed-normalized colour attachment is complete and round-trips its channel values" has to
|
||||||
|
// hold whichever path answers it, so the scenario fails if anyone ever routes these formats to a
|
||||||
|
// lossy storage on a driver where it IS live. The substitution itself can only be observed on a
|
||||||
|
// device without EXT_render_snorm (Mali Immortalis-G925).
|
||||||
|
//
|
||||||
|
// DirectGLES only, like the three-channel scenario next door: DirectVulkan resolves SNORM formats
|
||||||
|
// on its own terms and asserting Espryt's answers there would pin a coincidence.
|
||||||
|
|
||||||
|
#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 const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A uniform rather than a literal so nothing can constant-fold the value into a different
|
||||||
|
// precision than the one the attachment stores.
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
out vec4 oColor;
|
||||||
|
uniform float uValue;
|
||||||
|
void main() { oColor = vec4(uValue, 0.0, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr int kSize = 8;
|
||||||
|
|
||||||
|
// The two channel values the round trip is pinned on. Both are positive on purpose:
|
||||||
|
// glReadPixels applies GL_CLAMP_READ_COLOR (GL_FIXED_ONLY by default) to a fixed-point
|
||||||
|
// colour buffer, so the negative half of a SNORM attachment reads back as 0 and would
|
||||||
|
// measure the clamp instead of the storage.
|
||||||
|
constexpr int kSnorm8Value = 99;
|
||||||
|
constexpr int kSnorm16Value = 23451;
|
||||||
|
|
||||||
|
class SnormAttachmentScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (Gl().BackendName() != "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "the signed-normalized substitution is a DirectGLES fallback; backend is "
|
||||||
|
<< Gl().BackendName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single-level 2D texture in `internalFormat`, or 0 when the driver rejects the
|
||||||
|
// storage outright (which is a different failure from rejecting the ATTACHMENT).
|
||||||
|
static GLuint MakeTexture(GLenum internalFormat) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
|
||||||
|
if (glGetError() != GL_NO_ERROR) {
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLenum SingleAttachmentStatus(GLenum internalFormat) {
|
||||||
|
const GLuint texture = MakeTexture(internalFormat);
|
||||||
|
if (texture == 0) return GL_NONE;
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders `value` into the red channel of a fresh `internalFormat` attachment and hands
|
||||||
|
// back what glReadPixels sees. Returns false when the framebuffer never came up, which
|
||||||
|
// is the failure mode this scenario exists for - a draw into an incomplete framebuffer
|
||||||
|
// is dropped by the driver and leaves the caller reading the cleared texture.
|
||||||
|
bool RenderAndReadRed(GLenum internalFormat, float value, float* outRed) {
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kVS, kFS, &error);
|
||||||
|
EXPECT_NE(program, 0u) << error;
|
||||||
|
if (program == 0) return false;
|
||||||
|
const GLint valueLocation = glGetUniformLocation(program, "uValue");
|
||||||
|
EXPECT_GE(valueLocation, 0);
|
||||||
|
|
||||||
|
const GLuint texture = MakeTexture(internalFormat);
|
||||||
|
EXPECT_NE(texture, 0u) << "the driver refused the texture storage itself";
|
||||||
|
if (texture == 0) {
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||||
|
GLuint vao = 0;
|
||||||
|
GLuint vbo = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||||
|
glUseProgram(program);
|
||||||
|
glUniform1f(valueLocation, value);
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
// Cleared to zero so a dropped draw cannot be mistaken for a correct one.
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
|
||||||
|
if (outRed) *outRed = pixels[0];
|
||||||
|
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE regression gate for the frontend's answer. Every one of these used to be
|
||||||
|
// GL_FRAMEBUFFER_UNSUPPORTED on a driver without EXT_render_snorm, and nothing in the CTS
|
||||||
|
// (or in Iris) checks the status before drawing, so the failure was silent all the way to a
|
||||||
|
// readback of zeroes.
|
||||||
|
TEST_F(SnormAttachmentScenario, SignedNormalizedColorAttachmentsReportComplete) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
// GL_R8 is the control: colour-renderable in ES core, so it must pass with or without
|
||||||
|
// any substitution. If it ever fails, nothing below means anything.
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_R8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "GL_R8 is ES-core colour-renderable";
|
||||||
|
|
||||||
|
// The single-channel pair KHR-GL4x.texture_swizzle renders into for every SNORM source
|
||||||
|
// format - the whole 46-case failure.
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_R8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_R16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
// ...and the two- and four-channel siblings, which are what a shaderpack actually
|
||||||
|
// declares (Iris colortex buffers in RGBA16_SNORM).
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RG8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RG16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half: whatever storage answers for the attachment has to hold the channel value
|
||||||
|
// to the format's own precision. This is the assertion that fails if the 16-bit formats are
|
||||||
|
// ever routed to a half float - the substitute an implementer naturally reaches for, because
|
||||||
|
// it is what the 8-bit ones correctly use.
|
||||||
|
TEST_F(SnormAttachmentScenario, SignedNormalizedAttachmentsRoundTripTheirChannelValues) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
const float snorm8Expected = static_cast<float>(kSnorm8Value) / 127.0f;
|
||||||
|
float red8 = -1.0f;
|
||||||
|
ASSERT_TRUE(RenderAndReadRed(GL_R8_SNORM, snorm8Expected, &red8))
|
||||||
|
<< "an R8_SNORM colour attachment must be complete before any value can be asserted";
|
||||||
|
// Two 8-bit SNORM steps. A half float is exact here (worst case 0.03 of a step), so this
|
||||||
|
// only has to catch a storage that quantizes harder than the format itself.
|
||||||
|
EXPECT_NEAR(red8, snorm8Expected, 2.0f / 127.0f)
|
||||||
|
<< "R8_SNORM attachment lost its channel value";
|
||||||
|
EXPECT_GT(red8, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
||||||
|
|
||||||
|
const float snorm16Expected = static_cast<float>(kSnorm16Value) / 32767.0f;
|
||||||
|
float red16 = -1.0f;
|
||||||
|
ASSERT_TRUE(RenderAndReadRed(GL_R16_SNORM, snorm16Expected, &red16))
|
||||||
|
<< "an R16_SNORM colour attachment must be complete before any value can be asserted";
|
||||||
|
// Two 16-bit SNORM steps (6.1e-5). A half float would land 1.9e-4 away - three times
|
||||||
|
// this window - which is exactly the failure this bound exists to catch.
|
||||||
|
EXPECT_NEAR(red16, snorm16Expected, 2.0f / 32767.0f)
|
||||||
|
<< "R16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
||||||
|
EXPECT_GT(red16, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
||||||
|
|
||||||
|
float red16x4 = -1.0f;
|
||||||
|
ASSERT_TRUE(RenderAndReadRed(GL_RGBA16_SNORM, snorm16Expected, &red16x4))
|
||||||
|
<< "an RGBA16_SNORM colour attachment must be complete before any value can be asserted";
|
||||||
|
EXPECT_NEAR(red16x4, snorm16Expected, 2.0f / 32767.0f)
|
||||||
|
<< "RGBA16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
||||||
|
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -163,7 +163,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (!m_resource.IsGpuResident() &&
|
if (!m_resource.IsGpuResident() &&
|
||||||
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
||||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||||
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data(),
|
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data() + m_stagingBias,
|
||||||
m_mappedRange.end - m_mappedRange.start);
|
m_mappedRange.end - m_mappedRange.start);
|
||||||
}
|
}
|
||||||
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
||||||
@@ -175,6 +175,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_isMapped = false;
|
m_isMapped = false;
|
||||||
m_mappingAccess = BufferMappingAccessBit::Null;
|
m_mappingAccess = BufferMappingAccessBit::Null;
|
||||||
m_mappedRange = {0, 0};
|
m_mappedRange = {0, 0};
|
||||||
|
m_stagingBias = 0;
|
||||||
m_ownsStagingData = false;
|
m_ownsStagingData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
|
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
|
||||||
// the staged bytes must be copied into the shadow before the backend reads them.
|
// the staged bytes must be copied into the shadow before the backend reads them.
|
||||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||||
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + offset, length);
|
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + m_stagingBias + offset, length);
|
||||||
}
|
}
|
||||||
NotifyFlushMappedRange({start, end}, m_mappingAccess);
|
NotifyFlushMappedRange({start, end}, m_mappingAccess);
|
||||||
}
|
}
|
||||||
@@ -311,6 +312,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_mappedRange = {0, m_size};
|
m_mappedRange = {0, m_size};
|
||||||
|
|
||||||
if (m_mappingAccess & BufferMappingAccessBit::Write) {
|
if (m_mappingAccess & BufferMappingAccessBit::Write) {
|
||||||
|
// glMapBuffer maps from offset 0, so no bias: the allocation's own
|
||||||
|
// GL_MIN_MAP_BUFFER_ALIGNMENT-aligned base is what the application must get.
|
||||||
|
m_stagingBias = 0;
|
||||||
m_stagingData.resize(m_size);
|
m_stagingData.resize(m_size);
|
||||||
m_ownsStagingData = true;
|
m_ownsStagingData = true;
|
||||||
|
|
||||||
@@ -372,14 +376,21 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (access & BufferMappingAccessBit::Write) {
|
if (access & BufferMappingAccessBit::Write) {
|
||||||
m_stagingData.resize(range.end - range.start);
|
// ARB_map_buffer_alignment constrains (returned pointer - offset), not the pointer:
|
||||||
|
// a map at offset 63 must hand back a pointer 63 bytes past the alignment grid, which
|
||||||
|
// is exactly what the read path below gets for free from shadowBase + offset. The
|
||||||
|
// staging store has to be biased by the same phase to match, so it over-allocates by
|
||||||
|
// it and the mapped bytes start at data() + m_stagingBias.
|
||||||
|
m_stagingBias = range.start % MIN_MAP_BUFFER_ALIGNMENT;
|
||||||
|
const SizeT mappedLength = range.end - range.start;
|
||||||
|
m_stagingData.resize(m_stagingBias + mappedLength);
|
||||||
m_ownsStagingData = true;
|
m_ownsStagingData = true;
|
||||||
|
|
||||||
if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) {
|
if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) {
|
||||||
Memcpy(m_stagingData.data(), m_resource.Bytes() + range.start, m_stagingData.size());
|
Memcpy(m_stagingData.data() + m_stagingBias, m_resource.Bytes() + range.start, mappedLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_stagingData.data();
|
return m_stagingData.data() + m_stagingBias;
|
||||||
} else {
|
} else {
|
||||||
m_ownsStagingData = false;
|
m_ownsStagingData = false;
|
||||||
return m_resource.Bytes() + range.start;
|
return m_resource.Bytes() + range.start;
|
||||||
@@ -438,7 +449,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
||||||
}
|
}
|
||||||
if (m_ownsStagingData) {
|
if (m_ownsStagingData) {
|
||||||
return const_cast<Uint8*>(m_stagingData.data());
|
return const_cast<Uint8*>(m_stagingData.data()) + m_stagingBias;
|
||||||
}
|
}
|
||||||
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,14 @@ namespace MobileGL {
|
|||||||
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||||
Bool m_gpuWritePending = false;
|
Bool m_gpuWritePending = false;
|
||||||
Range1D m_mappedRange;
|
Range1D m_mappedRange;
|
||||||
Vector<Uint8> m_stagingData;
|
// The write-map staging store. MapAlignedData because the application is handed a
|
||||||
|
// pointer into it, and biased by m_stagingBias because ARB_map_buffer_alignment
|
||||||
|
// requires (returned pointer - offset) to be aligned, not the pointer itself: a range
|
||||||
|
// map at offset 63 must hand back a pointer sitting 63 bytes past the alignment grid.
|
||||||
|
// The bias is the offset's phase, so the mapped bytes still start at
|
||||||
|
// m_stagingData.data() + m_stagingBias and the allocation is that much longer.
|
||||||
|
MapAlignedData m_stagingData;
|
||||||
|
SizeT m_stagingBias = 0;
|
||||||
Bool m_ownsStagingData;
|
Bool m_ownsStagingData;
|
||||||
};
|
};
|
||||||
} // namespace MG_State::GLState
|
} // namespace MG_State::GLState
|
||||||
|
|||||||
@@ -10,8 +10,56 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Util/Types.h>
|
#include <MG_Util/Types.h>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
|
#include <new>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// GL_MIN_MAP_BUFFER_ALIGNMENT. GL 4.2 / ARB_map_buffer_alignment fix the minimum at 64 and
|
||||||
|
// MobileGL advertises exactly that (MG_Impl/GLImpl/Getter/GL_Getter.cpp reads this constant),
|
||||||
|
// so under-reporting is not available - the implementation has to be brought up to the number
|
||||||
|
// instead. The promise is about POINTERS, not just the query: glMapBuffer must return a
|
||||||
|
// 64-byte-aligned pointer, and glMapBufferRange must return one whose base - the returned
|
||||||
|
// pointer minus the offset the caller asked for - is. Every pointer the frontend hands out
|
||||||
|
// comes from the shadow below or from BufferObject's staging buffer, and std::vector only
|
||||||
|
// promises alignof(std::max_align_t) (16 on aarch64), so both allocations carry the alignment
|
||||||
|
// themselves. One constant for the getter and the allocator, because the two may never
|
||||||
|
// disagree - the same reason the atomic-counter limits are shared through
|
||||||
|
// MG_Util/ShaderTranspiler/Types.h.
|
||||||
|
inline constexpr SizeT MIN_MAP_BUFFER_ALIGNMENT = 64;
|
||||||
|
|
||||||
|
// Allocator that gives every allocation MIN_MAP_BUFFER_ALIGNMENT. Deliberately minimal: the
|
||||||
|
// vectors it backs hold raw bytes and are only ever sized, so allocate/deallocate plus the
|
||||||
|
// rebinding and equality boilerplate std::vector requires is the whole interface.
|
||||||
|
template <typename T>
|
||||||
|
struct MapAlignedAllocator {
|
||||||
|
using value_type = T;
|
||||||
|
|
||||||
|
MapAlignedAllocator() noexcept = default;
|
||||||
|
template <typename U>
|
||||||
|
MapAlignedAllocator(const MapAlignedAllocator<U>&) noexcept {}
|
||||||
|
|
||||||
|
T* allocate(SizeT count) {
|
||||||
|
if (count == 0) return nullptr;
|
||||||
|
return static_cast<T*>(
|
||||||
|
::operator new(count * sizeof(T), std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT}));
|
||||||
|
}
|
||||||
|
void deallocate(T* pointer, SizeT) noexcept {
|
||||||
|
::operator delete(pointer, std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename U>
|
||||||
|
Bool operator==(const MapAlignedAllocator<U>&) const noexcept {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
template <typename U>
|
||||||
|
Bool operator!=(const MapAlignedAllocator<U>&) const noexcept {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Byte store for anything the application may end up holding a mapped pointer into.
|
||||||
|
using MapAlignedData = std::vector<Uint8, MapAlignedAllocator<Uint8>>;
|
||||||
|
|
||||||
// Opaque, refcounted handle to the backend's GPU storage for one buffer
|
// Opaque, refcounted handle to the backend's GPU storage for one buffer
|
||||||
// (the driver-side resource). The active backend derives from it and attaches
|
// (the driver-side resource). The active backend derives from it and attaches
|
||||||
// its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource.
|
// its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource.
|
||||||
@@ -57,8 +105,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
// Direct shadow access, used only by the backend's upload-from-shadow path,
|
// Direct shadow access, used only by the backend's upload-from-shadow path,
|
||||||
// which never runs for a GPU-resident (persistent) buffer.
|
// which never runs for a GPU-resident (persistent) buffer.
|
||||||
Data& Shadow() { return *m_shadow; }
|
MapAlignedData& Shadow() { return *m_shadow; }
|
||||||
const Data& Shadow() const { return *m_shadow; }
|
const MapAlignedData& Shadow() const { return *m_shadow; }
|
||||||
|
|
||||||
// Transition to persistent GPU residency: adopt the backend's coherent
|
// Transition to persistent GPU residency: adopt the backend's coherent
|
||||||
// mapped base as the source of truth and drop the CPU shadow. The caller
|
// mapped base as the source of truth and drop the CPU shadow. The caller
|
||||||
@@ -85,7 +133,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); }
|
SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SharedPtr<Data> m_shadow = MakeShared<Data>();
|
// MapAlignedData, not Data: a read-only glMapBuffer hands the application this very
|
||||||
|
// pointer, and a range map hands it base + offset, so the base has to be on the
|
||||||
|
// GL_MIN_MAP_BUFFER_ALIGNMENT grid for either to satisfy ARB_map_buffer_alignment.
|
||||||
|
SharedPtr<MapAlignedData> m_shadow = MakeShared<MapAlignedData>();
|
||||||
void* m_gpuMapped = nullptr;
|
void* m_gpuMapped = nullptr;
|
||||||
SharedPtr<BackendBufferResource> m_backend;
|
SharedPtr<BackendBufferResource> m_backend;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -368,6 +368,31 @@ namespace MobileGL {
|
|||||||
return m_transformFeedbackGeometryCaptureDraws;
|
return m_transformFeedbackGeometryCaptureDraws;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conditional rendering (GL 4.6 core 10.9). `discard` is the verdict already
|
||||||
|
// resolved from the query object at glBeginConditionalRender - the predicate is
|
||||||
|
// read ONCE there, not per command, because GL specifies the block against the
|
||||||
|
// result available at Begin and re-reading it would let a query that is still
|
||||||
|
// being written change the answer mid-block.
|
||||||
|
void BeginConditionalRender(GLuint queryId, GLenum mode, Bool discard) {
|
||||||
|
m_conditionalRenderActive = true;
|
||||||
|
m_conditionalRenderQuery = queryId;
|
||||||
|
m_conditionalRenderMode = mode;
|
||||||
|
m_conditionalRenderDiscards = discard;
|
||||||
|
}
|
||||||
|
void EndConditionalRender() {
|
||||||
|
m_conditionalRenderActive = false;
|
||||||
|
m_conditionalRenderQuery = 0;
|
||||||
|
m_conditionalRenderMode = GL_NONE;
|
||||||
|
m_conditionalRenderDiscards = false;
|
||||||
|
}
|
||||||
|
Bool IsConditionalRenderActive() const { return m_conditionalRenderActive; }
|
||||||
|
GLuint GetConditionalRenderQuery() const { return m_conditionalRenderQuery; }
|
||||||
|
// Whether the commands GL 4.6 core 10.9 makes conditional are being discarded
|
||||||
|
// right now. False whenever no block is open, so a caller needs no second test.
|
||||||
|
Bool ConditionalRenderDiscardsCommands() const {
|
||||||
|
return m_conditionalRenderActive && m_conditionalRenderDiscards;
|
||||||
|
}
|
||||||
|
|
||||||
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
||||||
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
||||||
// binding points are object state, but the context keeps exactly one live
|
// binding points are object state, but the context keeps exactly one live
|
||||||
@@ -466,6 +491,13 @@ namespace MobileGL {
|
|||||||
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
|
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
|
||||||
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
|
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
|
||||||
|
|
||||||
|
// Conditional rendering. Context state, not object state: GL 4.6 core 10.9 allows
|
||||||
|
// exactly one block open at a time and no object owns it.
|
||||||
|
Bool m_conditionalRenderActive = false;
|
||||||
|
Bool m_conditionalRenderDiscards = false;
|
||||||
|
GLuint m_conditionalRenderQuery = 0;
|
||||||
|
GLenum m_conditionalRenderMode = GL_NONE;
|
||||||
|
|
||||||
// Everything a transform feedback object owns while it is NOT the bound one.
|
// Everything a transform feedback object owns while it is NOT the bound one.
|
||||||
struct TransformFeedbackObjectState {
|
struct TransformFeedbackObjectState {
|
||||||
struct SavedBufferBinding {
|
struct SavedBufferBinding {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "ProgramLinkTask.h"
|
#include "ProgramLinkTask.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||||
|
|
||||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
@@ -36,6 +38,70 @@ namespace {
|
|||||||
return std::min(backendLimit, capacity);
|
return std::min(backendLimit, capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Everything the post-link query surface ever asks a glslang::TType, flattened into a
|
||||||
|
// POD. The list is closed and was audited call site by call site: nothing after the link
|
||||||
|
// walks a struct, a type name or the AST, so there is no recursion to mirror.
|
||||||
|
//
|
||||||
|
// Why it has to be flattened at all: TObjectReflection::type points into the TProgram's
|
||||||
|
// OWN TPoolAllocator (reflection.cpp clones each TType into it), so every one of these
|
||||||
|
// pointers dangles the moment the TProgram is released - and releasing it is exactly what
|
||||||
|
// lets a link be served from the L1 translation memo without a parse.
|
||||||
|
static MobileGL::MG_State::GLState::ProgramObject::TypeFacts MakeTypeFacts(const glslang::TType* type) {
|
||||||
|
MobileGL::MG_State::GLState::ProgramObject::TypeFacts facts;
|
||||||
|
if (type == nullptr) return facts;
|
||||||
|
facts.isArray = type->isArray();
|
||||||
|
facts.isSizedArray = type->isSizedArray();
|
||||||
|
facts.isMatrix = type->isMatrix();
|
||||||
|
facts.isVector = type->isVector();
|
||||||
|
facts.isOpaque = type->isOpaque();
|
||||||
|
facts.isTexture = type->isTexture();
|
||||||
|
facts.isImage = type->isImage();
|
||||||
|
facts.isDouble = type->getBasicType() == glslang::EbtDouble;
|
||||||
|
facts.isVoid = type->getBasicType() == glslang::EbtVoid;
|
||||||
|
facts.basicType = static_cast<MobileGL::Int>(type->getBasicType());
|
||||||
|
// Stored RAW, exactly as glslang reports them (0 for a non-matrix, 1 for a scalar),
|
||||||
|
// because the callers already gate on isMatrix()/isVector() themselves.
|
||||||
|
facts.vectorSize = type->getVectorSize();
|
||||||
|
facts.matrixCols = type->getMatrixCols();
|
||||||
|
facts.matrixRows = type->getMatrixRows();
|
||||||
|
const glslang::TQualifier& qualifier = type->getQualifier();
|
||||||
|
facts.isBuffer = qualifier.storage == glslang::EvqBuffer;
|
||||||
|
facts.isPatch = qualifier.patch;
|
||||||
|
facts.hasIndex = qualifier.hasIndex();
|
||||||
|
facts.layoutIndex = static_cast<MobileGL::Int>(qualifier.layoutIndex);
|
||||||
|
facts.hasFormat = qualifier.hasFormat();
|
||||||
|
facts.layoutFormat = static_cast<MobileGL::Uint>(qualifier.getFormat());
|
||||||
|
facts.layoutMatrix = static_cast<MobileGL::Int>(qualifier.layoutMatrix);
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One glslang::TObjectReflection, flattened. Shared by uniforms, blocks, pipe inputs and
|
||||||
|
// pipe outputs, because glslang reflects all four as TObjectReflection.
|
||||||
|
static MobileGL::MG_State::GLState::ProgramObject::ResourceReflection MakeResourceReflection(
|
||||||
|
const glslang::TObjectReflection& object) {
|
||||||
|
MobileGL::MG_State::GLState::ProgramObject::ResourceReflection record;
|
||||||
|
record.name = object.name;
|
||||||
|
record.glDefineType = object.glDefineType;
|
||||||
|
record.offset = object.offset;
|
||||||
|
record.size = object.size;
|
||||||
|
record.index = object.index;
|
||||||
|
record.counterIndex = object.counterIndex;
|
||||||
|
record.arrayStride = object.arrayStride;
|
||||||
|
record.topLevelArraySize = object.topLevelArraySize;
|
||||||
|
record.topLevelArrayStride = object.topLevelArrayStride;
|
||||||
|
record.binding = object.getBinding();
|
||||||
|
record.location = object.layoutLocation();
|
||||||
|
record.stages = static_cast<MobileGL::Uint32>(object.stages);
|
||||||
|
record.type = MakeTypeFacts(object.getType());
|
||||||
|
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, resolved here so no caller needs the TType:
|
||||||
|
// TObjectReflection::size carries the element count only for a NON-block array, so
|
||||||
|
// the sized-array outer count wins whenever it exists.
|
||||||
|
const glslang::TType* type = object.getType();
|
||||||
|
record.arraySize = (type != nullptr && type->isSizedArray()) ? type->getOuterArraySize()
|
||||||
|
: (object.size < 1 ? 1 : object.size);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
static MobileGL::String StripArrayElementSuffix(const MobileGL::String& name) {
|
static MobileGL::String StripArrayElementSuffix(const MobileGL::String& name) {
|
||||||
const MobileGL::SizeT bracket = name.find('[');
|
const MobileGL::SizeT bracket = name.find('[');
|
||||||
return bracket == MobileGL::String::npos ? name : name.substr(0, bracket);
|
return bracket == MobileGL::String::npos ? name : name.substr(0, bracket);
|
||||||
@@ -63,6 +129,180 @@ namespace {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 7.7 / ARB_shader_atomic_counters: within one binding no two atomic counters
|
||||||
|
// may occupy the same bytes, every offset is a multiple of 4, and no counter may reach past
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces all three in fixOffset(), which the
|
||||||
|
// Vulkan-relaxed parse never reaches - vkRelaxedRemapUniformVariable folds the atomic_uint
|
||||||
|
// into a synthesized storage block and returns from declareVariable() before fixOffset()
|
||||||
|
// runs, clearing explicitOffset on the way ("xxTODO: use logic from fixOffset()"). Two
|
||||||
|
// counters declared at the same binding AND the same offset therefore linked cleanly.
|
||||||
|
//
|
||||||
|
// The offsets themselves survive that lowering (reflection and the SPIR-V generator both
|
||||||
|
// honour layoutOffset), so the check belongs here, over the same model the GL queries answer
|
||||||
|
// from. Returns the info-log line for an illegal layout, empty for a legal one.
|
||||||
|
static MobileGL::String ValidateAtomicCounterLayout(glslang::TProgram& reflection) {
|
||||||
|
using MobileGL::Bool;
|
||||||
|
using MobileGL::Int;
|
||||||
|
using MobileGL::SizeT;
|
||||||
|
using MobileGL::String;
|
||||||
|
using MobileGL::Vector;
|
||||||
|
namespace Transpiler = MobileGL::MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const Int blockCount = reflection.getNumUniformBlocks();
|
||||||
|
if (blockCount <= 0) return {};
|
||||||
|
const SizeT prefixLength = std::strlen(Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX);
|
||||||
|
Vector<Bool> isCounterBlock(static_cast<SizeT>(blockCount), false);
|
||||||
|
Bool anyCounterBlock = false;
|
||||||
|
for (Int i = 0; i < blockCount; ++i) {
|
||||||
|
const auto& block = reflection.getUniformBlock(i);
|
||||||
|
isCounterBlock[static_cast<SizeT>(i)] =
|
||||||
|
block.name.compare(0, prefixLength, Transpiler::ATOMIC_COUNTER_BLOCK_PREFIX) == 0;
|
||||||
|
anyCounterBlock = anyCounterBlock || isCounterBlock[static_cast<SizeT>(i)];
|
||||||
|
}
|
||||||
|
if (!anyCounterBlock) return {}; // every program that declares no atomic counter
|
||||||
|
|
||||||
|
struct CounterSpan {
|
||||||
|
Int offset = 0;
|
||||||
|
Int size = 0;
|
||||||
|
String name;
|
||||||
|
};
|
||||||
|
Vector<Vector<CounterSpan>> spansByBlock(static_cast<SizeT>(blockCount));
|
||||||
|
const Int uniformCount = reflection.getNumUniformVariables();
|
||||||
|
for (Int i = 0; i < uniformCount; ++i) {
|
||||||
|
const auto& uniform = reflection.getUniform(i);
|
||||||
|
const Int owner = uniform.index;
|
||||||
|
if (owner < 0 || owner >= blockCount || !isCounterBlock[static_cast<SizeT>(owner)]) continue;
|
||||||
|
const Int offset = uniform.offset;
|
||||||
|
if (offset < 0) continue; // no offset recorded; nothing to compare
|
||||||
|
Int elements = uniform.size > 1 ? uniform.size : 1;
|
||||||
|
if (const glslang::TType* type = uniform.getType(); type != nullptr && type->isArray()) {
|
||||||
|
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
|
||||||
|
}
|
||||||
|
const Int size = elements * static_cast<Int>(sizeof(MobileGL::Uint32));
|
||||||
|
if (offset % 4 != 0) {
|
||||||
|
return std::format("Atomic counter '{}' is declared at offset {}, which is not a multiple of 4.",
|
||||||
|
uniform.name, offset);
|
||||||
|
}
|
||||||
|
if (offset > Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE - size) {
|
||||||
|
return std::format("Atomic counter '{}' ends at byte {}, past the {}-byte "
|
||||||
|
"GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.",
|
||||||
|
uniform.name, offset + size, Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
auto& spans = spansByBlock[static_cast<SizeT>(owner)];
|
||||||
|
for (const CounterSpan& existing : spans) {
|
||||||
|
if (offset < existing.offset + existing.size && existing.offset < offset + size) {
|
||||||
|
return std::format("Atomic counters '{}' and '{}' share a binding and overlap at byte offset {}.",
|
||||||
|
existing.name, uniform.name, std::max(offset, existing.offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spans.push_back({offset, size, uniform.name});
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 7.6: LinkProgram FAILS when a stage's count of active image uniforms exceeds
|
||||||
|
// GL_MAX_{VERTEX,TESS_CONTROL,TESS_EVALUATION,GEOMETRY,FRAGMENT,COMPUTE}_IMAGE_UNIFORMS, or
|
||||||
|
// when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing enforced it: glslang carries
|
||||||
|
// those numbers in TBuiltInResource only so gl_Max*ImageUniforms can expand from them, and
|
||||||
|
// its linker never counts uniforms against them - so a program declaring one image uniform
|
||||||
|
// more than the limit linked cleanly and then rendered nothing.
|
||||||
|
//
|
||||||
|
// The limits are the ones glGetIntegerv answers (MG_Impl/GLImpl/Getter/GL_Getter.cpp), the
|
||||||
|
// hardcoded tessellation zeros included: a program may not exceed a limit the implementation
|
||||||
|
// advertises, whatever the driver underneath would have taken.
|
||||||
|
//
|
||||||
|
// Counts the APPLICATION's image uniforms. The DirectGLES read/write split emits a second
|
||||||
|
// declaration for an image a stage both reads and writes (MG_Backend/DirectGLES/Utils.h), but
|
||||||
|
// that happens in the backend after this link, and counting the expanded set here would
|
||||||
|
// reject programs that are legal by the numbers GL advertises. Returns the info-log line for
|
||||||
|
// a program over a limit, empty for one within them.
|
||||||
|
static MobileGL::String ValidateImageUniformLimits(
|
||||||
|
glslang::TProgram& reflection, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
|
||||||
|
using MobileGL::Array;
|
||||||
|
using MobileGL::Int;
|
||||||
|
using MobileGL::SizeT;
|
||||||
|
using MobileGL::UnorderedMap;
|
||||||
|
|
||||||
|
static constexpr EShLanguage kStages[] = {EShLangVertex, EShLangTessControl, EShLangTessEvaluation,
|
||||||
|
EShLangGeometry, EShLangFragment, EShLangCompute};
|
||||||
|
static constexpr const char* kLimitNames[] = {
|
||||||
|
"GL_MAX_VERTEX_IMAGE_UNIFORMS", "GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS",
|
||||||
|
"GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS", "GL_MAX_GEOMETRY_IMAGE_UNIFORMS",
|
||||||
|
"GL_MAX_FRAGMENT_IMAGE_UNIFORMS", "GL_MAX_COMPUTE_IMAGE_UNIFORMS"};
|
||||||
|
constexpr SizeT kStageCount = sizeof(kStages) / sizeof(kStages[0]);
|
||||||
|
const Int limits[kStageCount] = {env.params.MaxVertexImageUniforms,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
env.params.MaxGeometryImageUniforms,
|
||||||
|
env.params.MaxFragmentImageUniforms,
|
||||||
|
env.params.MaxComputeImageUniforms};
|
||||||
|
|
||||||
|
// Reflection spells an image ARRAY one of two ways, and which one it picks depends on how
|
||||||
|
// the shader indexed it: a variable index makes glslang expand the array into one entry
|
||||||
|
// per element ("u_image[0]".."u_image[8]", each carrying the ELEMENT type), while an
|
||||||
|
// array never dereferenced at all stays a single entry carrying the array type. One
|
||||||
|
// program can even produce both spellings for the same array. So neither counting entries
|
||||||
|
// nor trusting the declared size is right on its own - they are reconciled per declared
|
||||||
|
// name with a max, which is exact for either spelling and cannot double-count the mixture.
|
||||||
|
struct ImageUse {
|
||||||
|
Int entries = 0; // reflection entries seen for this name in this stage
|
||||||
|
Int declared = 0; // largest element count any of them declared
|
||||||
|
};
|
||||||
|
UnorderedMap<MobileGL::String, Array<ImageUse, kStageCount>> useByName;
|
||||||
|
|
||||||
|
const Int uniformCount = reflection.getNumUniformVariables();
|
||||||
|
for (Int i = 0; i < uniformCount; ++i) {
|
||||||
|
const auto& uniform = reflection.getUniform(i);
|
||||||
|
const glslang::TType* type = uniform.getType();
|
||||||
|
if (type == nullptr || !type->isImage()) continue;
|
||||||
|
// An array occupies one image unit per element; an unsized one (never indexed, so
|
||||||
|
// never more than the single element glslang kept) counts as one.
|
||||||
|
Int elements = uniform.size > 1 ? uniform.size : 1;
|
||||||
|
if (type->isArray()) {
|
||||||
|
elements = type->isSizedArray() ? type->getCumulativeArraySize() : 1;
|
||||||
|
}
|
||||||
|
// `stages` is the set of stages that REFERENCE the uniform, which is exactly what GL
|
||||||
|
// counts: an image declared in two stages costs a unit in each, and one no stage
|
||||||
|
// reads is not active at all and costs nothing.
|
||||||
|
Array<ImageUse, kStageCount>* use = nullptr;
|
||||||
|
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||||
|
if ((static_cast<unsigned>(uniform.stages) & (1u << static_cast<unsigned>(kStages[stage]))) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The one insert this uniform performs, so the reference survives the rest of the
|
||||||
|
// stage loop - a flat hash map relocates on insert, never on read.
|
||||||
|
if (use == nullptr) {
|
||||||
|
use = &useByName[StripArrayElementSuffix(uniform.name)];
|
||||||
|
}
|
||||||
|
++(*use)[stage].entries;
|
||||||
|
(*use)[stage].declared = std::max((*use)[stage].declared, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Int counts[kStageCount] = {};
|
||||||
|
for (const auto& entry : useByName) {
|
||||||
|
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||||
|
counts[stage] += std::max(entry.second[stage].entries, entry.second[stage].declared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Int combined = 0;
|
||||||
|
for (SizeT stage = 0; stage < kStageCount; ++stage) {
|
||||||
|
combined += counts[stage];
|
||||||
|
if (counts[stage] > limits[stage]) {
|
||||||
|
return std::format("This program uses {} active image uniforms in one stage, more than the {} "
|
||||||
|
"{} allows.",
|
||||||
|
counts[stage], limits[stage], kLimitNames[stage]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (combined > env.params.MaxCombinedImageUniforms) {
|
||||||
|
return std::format("This program uses {} active image uniforms across its stages, more than the {} "
|
||||||
|
"GL_MAX_COMBINED_IMAGE_UNIFORMS allows.",
|
||||||
|
combined, env.params.MaxCombinedImageUniforms);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
static bool IsBuiltInPipelineOutput(const glslang::TObjectReflection& output) {
|
||||||
const auto* type = output.getType();
|
const auto* type = output.getType();
|
||||||
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
return type && type->getQualifier().builtIn != glslang::EbvNone;
|
||||||
@@ -309,6 +549,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Link body start, shaders to link: %zu", in.externalIndex, in.shaders.size());
|
MGLOG_D("ProgramObject %u: Link body start, shaders to link: %zu", in.externalIndex, in.shaders.size());
|
||||||
|
|
||||||
|
if (!ValidateAttachedShaders()) return;
|
||||||
|
|
||||||
|
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
|
||||||
|
// run before the L1 probe, which needs the merged opaque bindings in its key.
|
||||||
|
MergeShaderSideChannels();
|
||||||
|
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
|
||||||
|
|
||||||
|
// ---- L1 of the shader translation memo ----
|
||||||
|
// Everything below this point - the parse, the link, mapIO, GlslangToSpv, spirv-opt,
|
||||||
|
// buildReflection and the global-UBO routing - is what a hit skips. See
|
||||||
|
// ProgramTranslationCache.h.
|
||||||
|
spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env);
|
||||||
|
if (TryPublishFromTranslationCache()) return;
|
||||||
|
|
||||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||||
if (!ConsumeShaders(shaders)) return;
|
if (!ConsumeShaders(shaders)) return;
|
||||||
|
|
||||||
@@ -335,30 +589,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge the shaders' lexically extracted explicit uniform locations. The same
|
|
||||||
// uniform declared in several stages must agree on its location (config-A glslang
|
|
||||||
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
|
|
||||||
for (const auto& shader : in.shaders) {
|
|
||||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
|
||||||
for (const auto& [name, location] : compiled.explicitUniformLocations) {
|
|
||||||
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
|
|
||||||
if (!inserted && it->second != location) {
|
|
||||||
artifacts.infoLog = std::format(
|
|
||||||
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
|
|
||||||
"across stages.",
|
|
||||||
name, it->second, location);
|
|
||||||
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sampler/image layout(binding = N) initial units, likewise invisible to the
|
|
||||||
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
|
|
||||||
// name declared in several stages keeps the last stage's binding as before.
|
|
||||||
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
|
|
||||||
artifacts.explicitOpaqueUniformBindings[name] = binding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgramAttrib attrib{.shaders = Move(shaders),
|
ProgramAttrib attrib{.shaders = Move(shaders),
|
||||||
.explicitVertexInLocations = in.explicitAttribLocations,
|
.explicitVertexInLocations = in.explicitAttribLocations,
|
||||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||||
@@ -497,14 +727,138 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
||||||
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
||||||
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
||||||
|
// The owned reflection mirror, and the block index space its global-UBO test needs.
|
||||||
|
// BuildGlobalUboRouting reads BOTH - per-uniform array size, opaqueness, GL type and
|
||||||
|
// matrix shape, plus "is this a member of a GL-visible block". Leaving them out of the
|
||||||
|
// handoff is not a compile error, it is a SILENT one: every array collapses to a
|
||||||
|
// single element and every element past the first falls through to the fallback tail
|
||||||
|
// allocator (ProgramTest.NestedStructArrayUniformElementWrites catches exactly that).
|
||||||
|
spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection;
|
||||||
|
spirvHandoff.reflection.blockReflection = artifacts.blockReflection;
|
||||||
|
spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl;
|
||||||
|
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
|
||||||
|
// A COPY, because the GL-thread join moves `artifacts` out of this node before phase B
|
||||||
|
// runs - and with the TProgram dropped, because a memo must never hold a glslang arena.
|
||||||
|
if (spirvHandoff.spirvCacheKey.Valid()) {
|
||||||
|
auto forCache = MakeShared<ProgramObject::LinkArtifacts>(artifacts);
|
||||||
|
forCache->program.reset();
|
||||||
|
spirvHandoff.linkArtifactsForCache = Move(forCache);
|
||||||
|
}
|
||||||
spirvHandoff.ready = true;
|
spirvHandoff.ready = true;
|
||||||
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
||||||
spirvHandoff.shaderTypes.size());
|
spirvHandoff.shaderTypes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
// The L1 key. Every input below is one that can change the SPIR-V this program
|
||||||
outShaders.assign(in.shaders.size(), nullptr);
|
// generates; see the key inventory on SpirvTranslationKeyInputs.
|
||||||
|
//
|
||||||
|
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
|
||||||
|
// classification on CompileEnv::frontendFingerprint, and L2's own key in
|
||||||
|
// MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||||
|
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
|
||||||
|
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
if (!ShaderTranslationCacheEnabled()) return {};
|
||||||
|
|
||||||
|
SpirvTranslationKeyInputs keyInputs;
|
||||||
|
// The FRONT-END fingerprint, not env.fingerprint: L1 must be shared by two contexts
|
||||||
|
// on different GPUs whenever glslang would produce the same thing for them. See the
|
||||||
|
// classification on CompileEnv::frontendFingerprint.
|
||||||
|
keyInputs.frontendFingerprint = env.frontendFingerprint;
|
||||||
|
// Always 0 on both production parse paths (ShaderCompileTask::RunCompilePipeline and
|
||||||
|
// ClaimParsedShader's re-parse). In the key regardless, so that a future non-zero
|
||||||
|
// value cannot alias a module parsed without it.
|
||||||
|
keyInputs.shaderCompileFlags = 0;
|
||||||
|
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
|
||||||
|
keyInputs.stages.reserve(in.shaders.size());
|
||||||
|
for (const LinkShaderInput& shader : in.shaders) {
|
||||||
|
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||||
|
if (compiled.preprocessedSource.empty()) {
|
||||||
|
// No text to key on - an internal shader object, or an artifact this build
|
||||||
|
// did not populate. Refuse to key rather than key on nothing.
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
keyInputs.stages.push_back(SpirvTranslationKeyInputs::Stage{
|
||||||
|
.type = MG_Util::ConvertShaderStageToGLEnum(shader.stage),
|
||||||
|
.preprocessedSource = StringView(compiled.preprocessedSource)});
|
||||||
|
}
|
||||||
|
if (keyInputs.stages.empty()) return {};
|
||||||
|
keyInputs.explicitVertexInLocations = &in.explicitAttribLocations;
|
||||||
|
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
|
||||||
|
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
|
||||||
|
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
|
||||||
|
// In the key ONLY because the payload now carries the reflection: transform feedback
|
||||||
|
// is resolved by reading the linked intermediates and never perturbs the generated
|
||||||
|
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
|
||||||
|
// gsStripTriangles, and maxFragmentOutputColorNumber decides whether the link is
|
||||||
|
// rejected at all. Widening a payload means widening the key.
|
||||||
|
keyInputs.requestedXfbVaryings = &in.requestedXfbVaryings;
|
||||||
|
keyInputs.xfbBufferMode = static_cast<Uint32>(in.requestedXfbBufferMode);
|
||||||
|
keyInputs.maxFragmentOutputColorNumber = in.maxFragmentOutputColorNumber;
|
||||||
|
return BuildSpirvTranslationKey(keyInputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The link rejections that need nothing but the compile snapshots. They run before the
|
||||||
|
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
|
||||||
|
// The two lexical side channels the relaxed parse cannot provide, merged across stages:
|
||||||
|
// explicit default-block uniform locations (which must agree, or the link fails) and
|
||||||
|
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
|
||||||
|
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
|
||||||
|
// of the L1 memo key.
|
||||||
|
void ProgramLinkTask::MergeShaderSideChannels() {
|
||||||
|
// Merge the shaders' lexically extracted explicit uniform locations. The same
|
||||||
|
// uniform declared in several stages must agree on its location (config-A glslang
|
||||||
|
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
|
||||||
|
for (const auto& shader : in.shaders) {
|
||||||
|
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||||
|
for (const auto& [name, location] : compiled.explicitUniformLocations) {
|
||||||
|
const auto [it, inserted] = artifacts.linkedExplicitUniformLocations.emplace(name, location);
|
||||||
|
if (!inserted && it->second != location) {
|
||||||
|
artifacts.infoLog = std::format(
|
||||||
|
"Uniform '{}' is declared with conflicting explicit locations ({} and {}) "
|
||||||
|
"across stages.",
|
||||||
|
name, it->second, location);
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sampler/image layout(binding = N) initial units, likewise invisible to the
|
||||||
|
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
|
||||||
|
// name declared in several stages keeps the last stage's binding as before.
|
||||||
|
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
|
||||||
|
artifacts.explicitOpaqueUniformBindings[name] = binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// An L1 hit: the entire front end, published without constructing a TShader or a
|
||||||
|
// TProgram. Everything here is a copy out of plain owned data - `link.program` is null in
|
||||||
|
// the payload by construction, and nothing reads it any more.
|
||||||
|
Bool ProgramLinkTask::TryPublishFromTranslationCache() {
|
||||||
|
if (!spirvHandoff.spirvCacheKey.Valid()) return false;
|
||||||
|
const ProgramTranslationResultPtr hit =
|
||||||
|
GetProgramTranslationCache().Find(spirvHandoff.spirvCacheKey);
|
||||||
|
if (!hit) return false;
|
||||||
|
|
||||||
|
artifacts = hit->link;
|
||||||
|
spirvHandoff.shaderTypes.resize(in.shaders.size());
|
||||||
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
|
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||||
|
}
|
||||||
|
// An ALIASING SharedPtr: it points at the payload's SpirvArtifacts while sharing
|
||||||
|
// ownership of the whole payload, so phase B publishes them without a second copy and
|
||||||
|
// without any chance of the entry being evicted from under it.
|
||||||
|
spirvHandoff.cachedSpirv =
|
||||||
|
SharedPtr<const ProgramObject::SpirvArtifacts>(hit, &hit->spirv);
|
||||||
|
spirvHandoff.ready = true;
|
||||||
|
MGLOG_D("ProgramObject %u: L1 cache hit - the whole front end was reused; no parse, no "
|
||||||
|
"link, no SPIR-V generation",
|
||||||
|
in.externalIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ProgramLinkTask::ValidateAttachedShaders() {
|
||||||
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
|
// GL 4.6 core 7.3: a compute shader may only be linked with other compute shaders -
|
||||||
// the compute pipeline has no other stages to link against, so a program that mixes
|
// the compute pipeline has no other stages to link against, so a program that mixes
|
||||||
// them must fail to link (KHR-GL43.compute_shader.api-program).
|
// them must fail to link (KHR-GL43.compute_shader.api-program).
|
||||||
@@ -526,8 +880,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const LinkShaderInput& input = in.shaders[i];
|
const LinkShaderInput& input = in.shaders[i];
|
||||||
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
||||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(input.compiled);
|
const ShaderCompileArtifacts& compiled = CompiledArtifacts(input.compiled);
|
||||||
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
|
|
||||||
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
|
||||||
|
|
||||||
if (!compiled.compileStatus) {
|
if (!compiled.compileStatus) {
|
||||||
// The compile log LEADS the quoted source, and that order is load-bearing:
|
// The compile log LEADS the quoted source, and that order is load-bearing:
|
||||||
@@ -546,6 +898,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
in.externalIndex, i, artifacts.infoLog));
|
in.externalIndex, i, artifacts.infoLog));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||||
|
outShaders.assign(in.shaders.size(), nullptr);
|
||||||
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
|
const LinkShaderInput& input = in.shaders[i];
|
||||||
|
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(input.stage);
|
||||||
|
MGLOG_D("ProgramObject %u: Preparing shader[%zu] stage %s", in.externalIndex, i,
|
||||||
|
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
||||||
String reparseLog;
|
String reparseLog;
|
||||||
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
|
outShaders[i] = input.compiled->ClaimParsedShader(reparseLog);
|
||||||
if (!outShaders[i]) {
|
if (!outShaders[i]) {
|
||||||
@@ -606,6 +969,22 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (String atomicCounterError = ValidateAtomicCounterLayout(*artifacts.program);
|
||||||
|
!atomicCounterError.empty()) {
|
||||||
|
artifacts.infoLog = Move(atomicCounterError);
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String imageUniformError = ValidateImageUniformLimits(*artifacts.program, env);
|
||||||
|
!imageUniformError.empty()) {
|
||||||
|
artifacts.infoLog = Move(imageUniformError);
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
|
// ---------- GL-facing index spaces (relaxed-parse cleanup) ----------
|
||||||
// Blocks first: global-UBO membership drives the uniform filter below. The
|
// Blocks first: global-UBO membership drives the uniform filter below. The
|
||||||
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
|
// synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL
|
||||||
@@ -658,7 +1037,16 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
||||||
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
|
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
|
||||||
// survives the relaxed parse (and mapIO auto-assigns the rest).
|
// survives the relaxed parse (and mapIO auto-assigns the rest).
|
||||||
constexpr Uint kNoLocation = glslang::TQualifier::layoutLocationEnd;
|
//
|
||||||
|
// "no effective location yet". Deliberately OUTSIDE the location space rather than
|
||||||
|
// glslang::TQualifier::layoutLocationEnd, which is the first location past the pool and
|
||||||
|
// therefore only one off a legal one - a sentinel that sits at the boundary it guards has
|
||||||
|
// to be re-proved safe every time the ceiling moves, and glslang uses that same value for
|
||||||
|
// "this opaque uniform has no location" as well.
|
||||||
|
constexpr Uint kNoLocation = ~static_cast<Uint>(0);
|
||||||
|
// The ceiling glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS) advertises, which is what the
|
||||||
|
// allocator below has to honour: locations 0..kMaxUniformLocations-1 and no others.
|
||||||
|
constexpr Uint kMaxUniformLocations = static_cast<Uint>(ProgramObject::MAX_UNIFORM_LOCATIONS);
|
||||||
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
|
Vector<Uint> effectiveLocation(tProgramUniformCount, kNoLocation);
|
||||||
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
|
Vector<Bool> locationIsSourceExplicit(tProgramUniformCount, false);
|
||||||
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
|
UnorderedMap<String, Uint> structExplicitCursor; // declared root -> next member location
|
||||||
@@ -695,13 +1083,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
|
cursor->second += static_cast<Uint>(GetUniformLocationSpan(uniform));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque()) {
|
// glslang parks "no location" at layoutLocationEnd, which is a real location in this
|
||||||
|
// table's numbering - test for it explicitly rather than letting it through as one.
|
||||||
|
if (effectiveLocation[i] == kNoLocation && type != nullptr && type->isOpaque() &&
|
||||||
|
uniform.layoutLocation() != glslang::TQualifier::layoutLocationEnd) {
|
||||||
effectiveLocation[i] = uniform.layoutLocation();
|
effectiveLocation[i] = uniform.layoutLocation();
|
||||||
}
|
}
|
||||||
if (locationIsSourceExplicit[i] &&
|
if (locationIsSourceExplicit[i] &&
|
||||||
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kNoLocation) {
|
effectiveLocation[i] + static_cast<Uint>(GetUniformLocationSpan(uniform)) > kMaxUniformLocations) {
|
||||||
// Config A rejected out-of-range explicit locations at parse; keep them
|
// Config A rejected out-of-range explicit locations at parse; keep them
|
||||||
// from growing the location table unboundedly.
|
// from growing the location table unboundedly. Stated against the advertised
|
||||||
|
// GL_MAX_UNIFORM_LOCATIONS, because that is the rule being enforced (GL 4.6 core
|
||||||
|
// 7.6.1): an array whose LAST element passes the ceiling is a link error even
|
||||||
|
// though its base compiled fine.
|
||||||
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
|
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
|
||||||
effectiveLocation[i]);
|
effectiveLocation[i]);
|
||||||
ProgramObject::ResetLinkArtifacts(artifacts);
|
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||||
@@ -709,12 +1103,55 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Int requiredUniformLocations = 0;
|
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: an explicit location is RESERVED
|
||||||
|
// whether or not the uniform turned out to be active. The dead default-block uniforms
|
||||||
|
// filtered out of glUniformIndexToTProgram above are invisible to every GL query - which
|
||||||
|
// is correct - but their locations must still be kept out of the implicit allocator's
|
||||||
|
// reach, or an implicit uniform is handed a location the source already claimed.
|
||||||
|
//
|
||||||
|
// Deliberately NOT written into artifacts.uniformLocations or uniformIndexInTProgram:
|
||||||
|
// glGetUniformLocation must keep answering -1 for a dead uniform, and a location no
|
||||||
|
// application can legally obtain must not become writable through glUniform*. The
|
||||||
|
// occupancy therefore lives in its own bitset, built once the table has been sized.
|
||||||
|
Vector<Pair<Uint, Int>> deadExplicitReservations;
|
||||||
|
Int deadReservedLocationCount = 0;
|
||||||
|
for (Int i = 0; i < tProgramUniformCount; i++) {
|
||||||
|
if (artifacts.tProgramUniformIndexToGl[i] >= 0) continue; // GL-visible: handled above
|
||||||
|
const auto& uniform = artifacts.program->getUniform(i);
|
||||||
|
if (!isGlobalUboMember(uniform) || uniform.stages != 0) continue;
|
||||||
|
const Int* explicitLocation = findExplicitLocation(uniform.name);
|
||||||
|
if (explicitLocation == nullptr) continue;
|
||||||
|
|
||||||
|
const Uint location = static_cast<Uint>(*explicitLocation);
|
||||||
|
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||||
|
if (location + static_cast<Uint>(locationSpan) > kMaxUniformLocations) {
|
||||||
|
artifacts.infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name,
|
||||||
|
location);
|
||||||
|
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
deadExplicitReservations.emplace_back(location, locationSpan);
|
||||||
|
deadReservedLocationCount += locationSpan;
|
||||||
|
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||||
|
MGLOG_D("ProgramObject %u: Reflection - inactive uniform '%s' reserves locations %u..%u without "
|
||||||
|
"becoming GL-visible",
|
||||||
|
in.externalIndex, uniform.name.c_str(), location, location + locationSpan - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Int requiredUniformLocations = deadReservedLocationCount;
|
||||||
|
// The same count restricted to DEFAULT-BLOCK uniforms, which is the only thing
|
||||||
|
// GL_MAX_UNIFORM_LOCATIONS bounds. requiredUniformLocations cannot serve: it also carries
|
||||||
|
// named-block members, which take a slot in this allocator's table (an implementation
|
||||||
|
// detail) but consume no GL uniform location at all, so a big UBO array would otherwise
|
||||||
|
// fail a link the spec allows.
|
||||||
|
Int defaultBlockLocationDemand = deadReservedLocationCount;
|
||||||
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
for (const Int i : artifacts.glUniformIndexToTProgram) {
|
||||||
auto& uniform = artifacts.program->getUniform(i);
|
auto& uniform = artifacts.program->getUniform(i);
|
||||||
const Uint location = effectiveLocation[i];
|
const Uint location = effectiveLocation[i];
|
||||||
const Int locationSpan = GetUniformLocationSpan(uniform);
|
const Int locationSpan = GetUniformLocationSpan(uniform);
|
||||||
requiredUniformLocations += locationSpan;
|
requiredUniformLocations += locationSpan;
|
||||||
|
const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform);
|
||||||
|
if (!inNamedBlock) defaultBlockLocationDemand += locationSpan;
|
||||||
if (location != kNoLocation) {
|
if (location != kNoLocation) {
|
||||||
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
artifacts.maxUniformLocation = std::max(artifacts.maxUniformLocation, location + locationSpan - 1);
|
||||||
}
|
}
|
||||||
@@ -727,6 +1164,22 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
|
MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d",
|
||||||
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength);
|
in.externalIndex, artifacts.maxUniformLocation, artifacts.uniformNameMaxLength);
|
||||||
|
|
||||||
|
// GL 4.6 core 7.6.1: explicit, implicit and reserved-but-inactive default-block uniforms
|
||||||
|
// all draw from the one GL_MAX_UNIFORM_LOCATIONS pool, and a program asking for more than
|
||||||
|
// the implementation advertises FAILS TO LINK
|
||||||
|
// (KHR-GL43.explicit_uniform_location.uniform-loc-negative-link-max-num-of-locations).
|
||||||
|
// A single uniform whose own span passes the ceiling was already rejected above; this is
|
||||||
|
// the aggregate half of the same rule.
|
||||||
|
if (defaultBlockLocationDemand > static_cast<Int>(kMaxUniformLocations)) {
|
||||||
|
artifacts.infoLog =
|
||||||
|
std::format("Uniform locations exhausted: the default-block uniforms need {} locations but "
|
||||||
|
"GL_MAX_UNIFORM_LOCATIONS is {}.",
|
||||||
|
defaultBlockLocationDemand, kMaxUniformLocations);
|
||||||
|
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
|
||||||
|
ProgramObject::ResetLinkArtifacts(artifacts);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
|
if (artifacts.maxUniformLocation + 1 < requiredUniformLocations) {
|
||||||
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
|
MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), "
|
||||||
"adjusting",
|
"adjusting",
|
||||||
@@ -741,6 +1194,27 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
glslang::TQualifier::layoutLocationEnd);
|
glslang::TQualifier::layoutLocationEnd);
|
||||||
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1);
|
artifacts.uniformSamplerOrImageUnitIndex.resize(artifacts.maxUniformLocation + 1, -1);
|
||||||
|
|
||||||
|
// Occupancy for the inactive explicit uniforms collected above: a set bit means "the
|
||||||
|
// source claimed this location", which is enough to keep the two implicit passes off it
|
||||||
|
// without making the location reachable through any GL entry point. A location the
|
||||||
|
// fallback grow path mints later is past this bitset by construction (every reservation
|
||||||
|
// was folded into maxUniformLocation before the table was sized), so the lookup treats
|
||||||
|
// out-of-range as free rather than resizing in lockstep.
|
||||||
|
// Left empty - and unallocated - when nothing reserved anything, which is every program in
|
||||||
|
// the shader-pack corpus; the lookup below reads an empty bitset as "nothing is reserved".
|
||||||
|
Vector<Bool> reservedLocation;
|
||||||
|
if (!deadExplicitReservations.empty()) {
|
||||||
|
reservedLocation.assign(artifacts.maxUniformLocation + 1, false);
|
||||||
|
for (const auto& [reservedBase, reservedSpan] : deadExplicitReservations) {
|
||||||
|
for (Int element = 0; element < reservedSpan; ++element) {
|
||||||
|
reservedLocation[reservedBase + element] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto locationIsReserved = [&reservedLocation](SizeT location) {
|
||||||
|
return location < reservedLocation.size() && reservedLocation[location];
|
||||||
|
};
|
||||||
|
|
||||||
Vector<int> unallocatedUniformIndex;
|
Vector<int> unallocatedUniformIndex;
|
||||||
|
|
||||||
// Pass 1: source-explicit locations. These are API contract
|
// Pass 1: source-explicit locations. These are API contract
|
||||||
@@ -785,7 +1259,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
|
Bool spanIsFree = location + locationSpan - 1 <= artifacts.maxUniformLocation;
|
||||||
for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
|
for (Int element = 0; spanIsFree && element < locationSpan; ++element) {
|
||||||
spanIsFree =
|
spanIsFree =
|
||||||
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd;
|
artifacts.uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd &&
|
||||||
|
!locationIsReserved(location + element);
|
||||||
}
|
}
|
||||||
if (!spanIsFree) {
|
if (!spanIsFree) {
|
||||||
artifacts.uniformLocations[uniform.name] = kNoLocation;
|
artifacts.uniformLocations[uniform.name] = kNoLocation;
|
||||||
@@ -817,7 +1292,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
|
bool hasRoom = locNeedle + locationSpan - 1 <= artifacts.maxUniformLocation;
|
||||||
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
|
for (Int element = 0; hasRoom && element < locationSpan; ++element) {
|
||||||
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
|
hasRoom = artifacts.uniformIndexInTProgram[locNeedle + element] ==
|
||||||
glslang::TQualifier::layoutLocationEnd;
|
glslang::TQualifier::layoutLocationEnd &&
|
||||||
|
!locationIsReserved(locNeedle + element);
|
||||||
}
|
}
|
||||||
if (!hasRoom) continue;
|
if (!hasRoom) continue;
|
||||||
// Found a vacant location at locNeedle
|
// Found a vacant location at locNeedle
|
||||||
@@ -987,9 +1463,78 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
|
||||||
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
ubo.name.c_str(), ubo.size, ubo.getBinding());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnapshotGlslangReflection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The last thing DoReflection does, and the thing that lets everything after it stop
|
||||||
|
// caring that a glslang::TProgram ever existed: copy every reflection record the GL query
|
||||||
|
// surface reads into LinkArtifacts' own owned tables.
|
||||||
|
//
|
||||||
|
// Indexed by TPROGRAM index throughout - the same space glUniformIndexToTProgram,
|
||||||
|
// tProgramUniformIndexToGl and uniformIndexInTProgram already speak - so the accessors
|
||||||
|
// that used to call program->getUniform(i) index uniformReflection[i] and are otherwise
|
||||||
|
// unchanged.
|
||||||
|
void ProgramLinkTask::SnapshotGlslangReflection() {
|
||||||
|
glslang::TProgram& program = *artifacts.program;
|
||||||
|
|
||||||
|
// Blocks FIRST: a uniform's effective layoutMatrix is resolved against its owning
|
||||||
|
// block below, which needs the block records to already exist.
|
||||||
|
const Int blockCount = program.getNumUniformBlocks();
|
||||||
|
artifacts.blockReflection.clear();
|
||||||
|
artifacts.blockReflection.reserve(static_cast<SizeT>(blockCount));
|
||||||
|
for (Int i = 0; i < blockCount; ++i) {
|
||||||
|
artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int uniformCount = program.getNumUniformVariables();
|
||||||
|
artifacts.uniformReflection.clear();
|
||||||
|
artifacts.uniformReflection.reserve(static_cast<SizeT>(uniformCount));
|
||||||
|
artifacts.uniformIndexByName.clear();
|
||||||
|
artifacts.uniformIndexByName.reserve(static_cast<SizeT>(uniformCount));
|
||||||
|
for (Int i = 0; i < uniformCount; ++i) {
|
||||||
|
ProgramObject::UniformReflection record = MakeResourceReflection(program.getUniform(i));
|
||||||
|
// A block-level layout(row_major)/(column_major) that the member did not inherit
|
||||||
|
// in its own qualifier. Resolved once HERE rather than at every GL_UNIFORM_* query,
|
||||||
|
// which is what the getUniformBlock() fallback in the old accessors was doing.
|
||||||
|
if (record.type.layoutMatrix == static_cast<Int>(glslang::ElmNone) && record.index >= 0 &&
|
||||||
|
record.index < static_cast<Int>(artifacts.blockReflection.size())) {
|
||||||
|
record.type.layoutMatrix = artifacts.blockReflection[record.index].type.layoutMatrix;
|
||||||
|
}
|
||||||
|
// Keyed on the REFLECTED name and on uniforms only. That is deliberate and is the
|
||||||
|
// filtered semantics the old code hand-rolled: glslang's TReflection::nameToIndex
|
||||||
|
// also holds block and function entries, which is exactly why every
|
||||||
|
// getUniformIndex() call site re-checked getUniform(idx).name == name afterwards.
|
||||||
|
// First writer wins, so a duplicated name resolves the way a forward scan would.
|
||||||
|
artifacts.uniformIndexByName.emplace(record.name, i);
|
||||||
|
artifacts.uniformReflection.push_back(Move(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int pipeInputCount = program.getNumPipeInputs();
|
||||||
|
artifacts.pipeInputReflection.clear();
|
||||||
|
artifacts.pipeInputReflection.reserve(static_cast<SizeT>(pipeInputCount));
|
||||||
|
for (Int i = 0; i < pipeInputCount; ++i) {
|
||||||
|
artifacts.pipeInputReflection.push_back(MakeResourceReflection(program.getPipeInput(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int pipeOutputCount = program.getNumPipeOutputs();
|
||||||
|
artifacts.pipeOutputReflection.clear();
|
||||||
|
artifacts.pipeOutputReflection.reserve(static_cast<SizeT>(pipeOutputCount));
|
||||||
|
for (Int i = 0; i < pipeOutputCount; ++i) {
|
||||||
|
artifacts.pipeOutputReflection.push_back(MakeResourceReflection(program.getPipeOutput(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts.lastStageIsFragment = program.getIntermediate(EShLangFragment) != nullptr;
|
||||||
|
for (Uint dim = 0; dim < 3u; ++dim) {
|
||||||
|
artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast<Int>(dim));
|
||||||
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: Reflection - snapshot: %zu uniform(s), %zu block(s), %zu input(s), "
|
||||||
|
"%zu output(s)",
|
||||||
|
in.externalIndex, artifacts.uniformReflection.size(), artifacts.blockReflection.size(),
|
||||||
|
artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size());
|
||||||
|
}
|
||||||
|
|
||||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||||
if (!artifacts.program) return false;
|
if (!artifacts.program) return false;
|
||||||
// The pipe-output list is the output interface of the program's LAST stage. Only a
|
// The pipe-output list is the output interface of the program's LAST stage. Only a
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
|
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h>
|
||||||
#include <MG_Util/Async/JobNode.h>
|
#include <MG_Util/Async/JobNode.h>
|
||||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
// One attached shader, as the link sees it: never the ShaderObject, always a snapshot.
|
// One attached shader, as the link sees it: never the ShaderObject, always a snapshot.
|
||||||
@@ -116,6 +117,27 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// for phase B after the join has moved `artifacts` away.
|
// for phase B after the join has moved `artifacts` away.
|
||||||
ProgramObject::LinkArtifacts reflection;
|
ProgramObject::LinkArtifacts reflection;
|
||||||
|
|
||||||
|
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||||
|
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||||
|
// A, and not by phase B - two reasons, both structural:
|
||||||
|
// * the key covers the four link-time request maps and the merged opaque
|
||||||
|
// bindings, and one of those (explicitOpaqueUniformBindings) lives in
|
||||||
|
// `artifacts`, which phase B is forbidden to read because the GL-thread join
|
||||||
|
// moves it out from under phase B;
|
||||||
|
// * built once, it serves both the lookup and the insert, so the program's
|
||||||
|
// sources are copied into the blob exactly once per link.
|
||||||
|
// Invalid (null blob) when the cache is disabled, or when a stage arrived
|
||||||
|
// without preprocessed source - in which case phase B simply translates.
|
||||||
|
MG_Util::ShaderTranspiler::TranslationCacheKey spirvCacheKey;
|
||||||
|
|
||||||
|
// Set on an L1 HIT: phase B publishes these SpirvArtifacts verbatim instead of
|
||||||
|
// generating anything. Null on a miss.
|
||||||
|
SharedPtr<const ProgramObject::SpirvArtifacts> cachedSpirv;
|
||||||
|
// Set on a MISS: the LinkArtifacts phase B has to pair with its own SpirvArtifacts
|
||||||
|
// to insert the completed front end. Copied here rather than read off the node,
|
||||||
|
// because the GL-thread join MOVES `artifacts` out before phase B runs.
|
||||||
|
SharedPtr<const ProgramObject::LinkArtifacts> linkArtifactsForCache;
|
||||||
|
|
||||||
// The one flag phase B tests before doing anything: false means this link never
|
// The one flag phase B tests before doing anything: false means this link never
|
||||||
// reached the tail of RunBody (it failed, or was cancelled mid-body).
|
// reached the tail of RunBody (it failed, or was cancelled mid-body).
|
||||||
Bool ready = false;
|
Bool ready = false;
|
||||||
@@ -142,8 +164,31 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ---- the link body, split exactly as ProgramObject::Link() had it ----
|
// ---- the link body, split exactly as ProgramObject::Link() had it ----
|
||||||
// Each returns false to abort the link with `artifacts.infoLog` already set, which is
|
// Each returns false to abort the link with `artifacts.infoLog` already set, which is
|
||||||
// GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error.
|
// GL's definition of a failed link: LINK_STATUS false plus a log, never a GL error.
|
||||||
|
// The two link-rejection gates that need no parsed shader: a compute stage mixed
|
||||||
|
// with any other, and an attached shader that failed to compile. Split out of
|
||||||
|
// ConsumeShaders so they still run - in the same order, with the same diagnostics -
|
||||||
|
// BEFORE the L1 memo is consulted, rather than behind a hit that would skip them.
|
||||||
|
// The two lexical side channels the relaxed parse cannot provide, merged across
|
||||||
|
// stages. Reads the compile snapshots only, so it runs before any parse - the merged
|
||||||
|
// opaque bindings are part of the L1 memo key. Sets artifacts.infoLog and leaves
|
||||||
|
// linkStatus false when two stages disagree on an explicit uniform location.
|
||||||
|
void MergeShaderSideChannels();
|
||||||
|
Bool ValidateAttachedShaders();
|
||||||
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
|
Bool ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders);
|
||||||
|
// Publishes a whole front end straight out of the L1 memo: no TShader, no TProgram,
|
||||||
|
// no SPIR-V generation. Returns false on a miss.
|
||||||
|
Bool TryPublishFromTranslationCache();
|
||||||
|
|
||||||
|
// The L1 memo key for the SPIR-V this program is about to generate, or an invalid
|
||||||
|
// key when the cache is off or a stage has no preprocessed source to key on.
|
||||||
|
// Called at the tail of RunBody, where every input it needs is still owned by this
|
||||||
|
// node and `artifacts` has not yet been published.
|
||||||
|
MG_Util::ShaderTranspiler::TranslationCacheKey BuildSpirvCacheKey(
|
||||||
|
const MG_Util::ShaderTranspiler::CompileEnv& env) const;
|
||||||
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
|
Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env);
|
||||||
|
// Copies every reflection record the GL query surface reads out of the glslang
|
||||||
|
// TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection.
|
||||||
|
void SnapshotGlslangReflection();
|
||||||
Bool ValidateFragmentOutputLocations();
|
Bool ValidateFragmentOutputLocations();
|
||||||
Bool ResolveTransformFeedbackVaryings();
|
Bool ResolveTransformFeedbackVaryings();
|
||||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||||
|
|||||||
@@ -615,15 +615,22 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
|
|
||||||
Int ProgramObject::GetFragmentDataLocation(const char* name) {
|
Int ProgramObject::GetFragmentDataLocation(const char* name) {
|
||||||
if (!Artifacts().program || !name) return -1;
|
// Answered from the OWNED pipe-output snapshot, not from Artifacts().program. The live
|
||||||
|
// TProgram is null on a translation-cache L1 hit - that is the entire point of the memo
|
||||||
|
// - and it is also null for any program that never linked. The old `if
|
||||||
|
// (!Artifacts().program) return -1` guard silently produced the never-linked answer for
|
||||||
|
// a perfectly good cached program, so glGetFragDataLocation returned -1 for every
|
||||||
|
// fragment output of it. The empty snapshot gives the never-linked case the same -1
|
||||||
|
// without needing the guard at all.
|
||||||
|
if (!name) return -1;
|
||||||
|
|
||||||
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
|
const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name);
|
||||||
const Int outputCount = Artifacts().program->getNumPipeOutputs();
|
for (const PipeOutputReflection& output : Artifacts().pipeOutputReflection) {
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
|
||||||
const auto& output = Artifacts().program->getPipeOutput(index);
|
|
||||||
if (output.name != name) continue;
|
if (output.name != name) continue;
|
||||||
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast<Int>(explicitLocation->second);
|
if (explicitLocation != Artifacts().linkedFragDataLocation.end()) {
|
||||||
return static_cast<Int>(output.layoutLocation());
|
return static_cast<Int>(explicitLocation->second);
|
||||||
|
}
|
||||||
|
return output.location;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,85 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
class ProgramObject {
|
class ProgramObject {
|
||||||
public:
|
public:
|
||||||
|
// GL_MAX_UNIFORM_LOCATIONS: locations 0 .. MAX_UNIFORM_LOCATIONS-1 are the whole legal
|
||||||
|
// range (GL 4.6 core 7.6.1 / ARB_explicit_uniform_location). Shared with GL_Getter rather
|
||||||
|
// than spelled twice, because the link and the query must agree exactly - the CTS declares
|
||||||
|
// a uniform at the advertised value minus one and expects it to link
|
||||||
|
// (KHR-GL43.explicit_uniform_location.uniform-loc-max).
|
||||||
|
//
|
||||||
|
// Tied to glslang's own ceiling and NOT raisable past it: ParseHelper rejects
|
||||||
|
// `layout(location = N)` for N >= TQualifier::layoutLocationEnd at COMPILE time, so
|
||||||
|
// layoutLocationEnd - 1 is the largest location any shader in this stack can declare -
|
||||||
|
// which makes exactly layoutLocationEnd locations, 0 .. layoutLocationEnd - 1, the pool.
|
||||||
|
// Advertising more would promise a location no shader could name. Comfortably above the
|
||||||
|
// 1024 GL 4.3 requires.
|
||||||
|
static constexpr Int MAX_UNIFORM_LOCATIONS = static_cast<Int>(glslang::TQualifier::layoutLocationEnd);
|
||||||
|
|
||||||
|
// Everything the query surface ever asked a glslang::TType, flattened. Twenty
|
||||||
|
// predicates, no recursion: nothing post-link ever walks a struct, a type name or the
|
||||||
|
// AST, so a POD covers the whole surface exactly.
|
||||||
|
struct TypeFacts {
|
||||||
|
Bool isArray = false;
|
||||||
|
// A runtime-sized array (a storage block's unsized trailing member) is an array
|
||||||
|
// that is NOT sized; GL_ARRAY_SIZE reports 0 for it.
|
||||||
|
Bool isSizedArray = false;
|
||||||
|
Bool isMatrix = false;
|
||||||
|
Bool isVector = false;
|
||||||
|
Bool isOpaque = false;
|
||||||
|
Bool isTexture = false;
|
||||||
|
Bool isImage = false;
|
||||||
|
Bool isDouble = false; // getBasicType() == EbtDouble
|
||||||
|
Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members)
|
||||||
|
Bool isBuffer = false; // getQualifier().storage == EvqBuffer
|
||||||
|
Bool isPatch = false; // getQualifier().patch
|
||||||
|
Bool hasIndex = false; // getQualifier().hasIndex()
|
||||||
|
Bool hasFormat = false; // getQualifier().hasFormat()
|
||||||
|
Int vectorSize = 0;
|
||||||
|
Int matrixCols = 0;
|
||||||
|
Int matrixRows = 0;
|
||||||
|
Int layoutIndex = 0; // getQualifier().layoutIndex
|
||||||
|
Uint layoutFormat = 0; // getQualifier().getFormat()
|
||||||
|
// glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against
|
||||||
|
// the owning block's qualifier, so the getUniformBlock() fallback the old
|
||||||
|
// accessors carried is gone.
|
||||||
|
Int layoutMatrix = 0;
|
||||||
|
// glslang::TBasicType, widened - ApplyUniformInitialValues and the typed
|
||||||
|
// glGetUniform* paths compare against a handful of enumerators.
|
||||||
|
Int basicType = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs
|
||||||
|
// and pipe outputs alike, because glslang reflects all four as TObjectReflection.
|
||||||
|
struct ResourceReflection {
|
||||||
|
String name;
|
||||||
|
GLenum glDefineType = 0;
|
||||||
|
Int offset = -1;
|
||||||
|
// TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is
|
||||||
|
// the resolved GL_UNIFORM_SIZE answer.
|
||||||
|
Int size = 0;
|
||||||
|
// TObjectReflection::index - for a uniform, the TPROGRAM block index owning it
|
||||||
|
// (-1 for a default-block one; translate with GlBlockIndexFromTProgram).
|
||||||
|
Int index = -1;
|
||||||
|
Int counterIndex = -1;
|
||||||
|
Int arrayStride = 0;
|
||||||
|
Int topLevelArraySize = 0;
|
||||||
|
Int topLevelArrayStride = 0;
|
||||||
|
Int binding = -1;
|
||||||
|
Int location = -1; // layoutLocation()
|
||||||
|
// EShLanguageMask of the stages that reference it; 0 means "declared but read by
|
||||||
|
// nobody", which is what the dead-default-block-uniform filter tests.
|
||||||
|
Uint32 stages = 0;
|
||||||
|
// GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the
|
||||||
|
// isSizedArray()/getOuterArraySize()/size fallback.
|
||||||
|
GLint arraySize = 1;
|
||||||
|
TypeFacts type;
|
||||||
|
};
|
||||||
|
|
||||||
|
using UniformReflection = ResourceReflection;
|
||||||
|
using BlockReflection = ResourceReflection;
|
||||||
|
using PipeInputReflection = ResourceReflection;
|
||||||
|
using PipeOutputReflection = ResourceReflection;
|
||||||
|
|
||||||
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
|
||||||
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
|
// Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an
|
||||||
// in-flight link whose program just went away is safe to abandon where it stands.
|
// in-flight link whose program just went away is safe to abandon where it stands.
|
||||||
@@ -134,8 +213,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const Int index = Artifacts().uniformIndexInTProgram[base];
|
const Int index = Artifacts().uniformIndexInTProgram[base];
|
||||||
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
// "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only
|
||||||
// in-range elements.
|
// in-range elements.
|
||||||
const glslang::TType* type = Artifacts().program->getUniform(index).getType();
|
if (!UniformAt(index).type.isArray) return -1;
|
||||||
if (type == nullptr || !type->isArray()) return -1;
|
|
||||||
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
|
if (static_cast<GLint>(element) >= GetUniformArraySizeByTIndex(index)) return -1;
|
||||||
const Int location = base + (Int)element;
|
const Int location = base + (Int)element;
|
||||||
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
if (!UniformLocationsAliasSameUniform(base, location)) return -1;
|
||||||
@@ -175,44 +253,35 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Int GetActiveUniformIndex(const String& name) const {
|
Int GetActiveUniformIndex(const String& name) const {
|
||||||
const Int tProgramCount = static_cast<Int>(Artifacts().tProgramUniformIndexToGl.size());
|
// uniformIndexByName is keyed by the REFLECTED name, so a lookup that hits is
|
||||||
const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str());
|
// already the exact-match the old code re-verified with a string compare after
|
||||||
if (uniformIndex >= 0 && uniformIndex < tProgramCount &&
|
// glslang's getUniformIndex(); a lookup that misses needs no bounds check.
|
||||||
Artifacts().program->getUniform(uniformIndex).name == name) {
|
const auto& byName = Artifacts().uniformIndexByName;
|
||||||
return GlUniformIndexFromTProgram(uniformIndex);
|
if (const auto direct = byName.find(name); direct != byName.end()) {
|
||||||
|
return GlUniformIndexFromTProgram(direct->second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
|
||||||
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
|
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
|
||||||
// robustness against non-suffixed reflection entries.
|
// robustness against non-suffixed reflection entries.
|
||||||
if (!name.empty() && name.back() != ']') {
|
if (!name.empty() && name.back() != ']') {
|
||||||
const String suffixedName = name + "[0]";
|
const auto suffixed = byName.find(name + "[0]");
|
||||||
const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str());
|
return suffixed != byName.end() ? GlUniformIndexFromTProgram(suffixed->second) : -1;
|
||||||
if (suffixedIndex >= 0 && suffixedIndex < tProgramCount &&
|
|
||||||
Artifacts().program->getUniform(suffixedIndex).name == suffixedName) {
|
|
||||||
return GlUniformIndexFromTProgram(suffixedIndex);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
|
||||||
const String baseName = name.substr(0, name.length() - 3);
|
const auto base = byName.find(name.substr(0, name.length() - 3));
|
||||||
const Int baseIndex = Artifacts().program->getUniformIndex(baseName.c_str());
|
return base != byName.end() ? GlUniformIndexFromTProgram(base->second) : -1;
|
||||||
if (baseIndex < 0 || baseIndex >= tProgramCount) return -1;
|
|
||||||
return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex)
|
|
||||||
: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); }
|
Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); }
|
||||||
|
|
||||||
GLenum GetUniformType(Uint location) const {
|
GLenum GetUniformType(Uint location) const {
|
||||||
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).glDefineType;
|
||||||
return uniform.glDefineType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLenum GetActiveUniformType(Uint index) const {
|
GLenum GetActiveUniformType(Uint index) const {
|
||||||
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
return UniformAt(TProgramUniformIndex(index)).glDefineType;
|
||||||
return uniform.glDefineType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
|
// Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array.
|
||||||
@@ -229,16 +298,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Int GetActiveUniformBlockIndex(Uint index) const {
|
Int GetActiveUniformBlockIndex(Uint index) const {
|
||||||
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
|
||||||
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
|
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
|
||||||
return GlBlockIndexFromTProgram(uniform.index);
|
return GlBlockIndexFromTProgram(UniformAt(TProgramUniformIndex(index)).index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
|
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
|
||||||
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
|
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
|
||||||
// seeing them as default-block uniforms, so gate on the GL-visible block index.
|
// seeing them as default-block uniforms, so gate on the GL-visible block index.
|
||||||
GLint GetActiveUniformOffset(Uint index) const {
|
GLint GetActiveUniformOffset(Uint index) const {
|
||||||
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
return uniform.offset;
|
return uniform.offset;
|
||||||
}
|
}
|
||||||
@@ -252,13 +320,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
|
||||||
// layout is always std140, where every array element stride rounds up to a vec4.
|
// layout is always std140, where every array element stride rounds up to a vec4.
|
||||||
GLint GetActiveUniformArrayStride(Uint index) const {
|
GLint GetActiveUniformArrayStride(Uint index) const {
|
||||||
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
const glslang::TType* type = uniform.getType();
|
if (!uniform.type.isArray) return 0;
|
||||||
if (type == nullptr || !type->isArray()) return 0;
|
if (uniform.type.isMatrix) {
|
||||||
if (type->isMatrix()) {
|
|
||||||
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
|
||||||
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols();
|
const int vectors = rowMajor ? uniform.type.matrixRows : uniform.type.matrixCols;
|
||||||
return GetActiveUniformMatrixStride(index) * vectors;
|
return GetActiveUniformMatrixStride(index) * vectors;
|
||||||
}
|
}
|
||||||
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
|
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
|
||||||
@@ -272,15 +339,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
|
// check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves
|
||||||
// an inheriting member's layoutMatrix == ElmNone.
|
// an inheriting member's layoutMatrix == ElmNone.
|
||||||
GLint GetActiveUniformIsRowMajor(Uint index) const {
|
GLint GetActiveUniformIsRowMajor(Uint index) const {
|
||||||
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0;
|
||||||
const glslang::TType* type = uniform.getType();
|
if (!uniform.type.isMatrix) return 0;
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
// layoutMatrix is already resolved against the owning block's qualifier at
|
||||||
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
// snapshot time, so the getUniformBlock() fallback this used to carry is gone.
|
||||||
if (layoutMatrix == glslang::ElmNone) {
|
return (uniform.type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor)) ? 1 : 0;
|
||||||
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
|
||||||
}
|
|
||||||
return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a
|
// GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a
|
||||||
@@ -290,16 +354,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
|
// out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For
|
||||||
// every GL 3.3 float matrix this evaluates to 16, independent of majorness.
|
// every GL 3.3 float matrix this evaluates to 16, independent of majorness.
|
||||||
GLint GetActiveUniformMatrixStride(Uint index) const {
|
GLint GetActiveUniformMatrixStride(Uint index) const {
|
||||||
const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
const auto& uniform = UniformAt(TProgramUniformIndex(index));
|
||||||
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1;
|
||||||
const glslang::TType* type = uniform.getType();
|
if (!uniform.type.isMatrix) return 0;
|
||||||
if (type == nullptr || !type->isMatrix()) return 0;
|
const bool rowMajor = (uniform.type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor));
|
||||||
glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix;
|
const int strideVectorComponents = rowMajor ? uniform.type.matrixCols : uniform.type.matrixRows;
|
||||||
if (layoutMatrix == glslang::ElmNone) {
|
|
||||||
layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix;
|
|
||||||
}
|
|
||||||
const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor);
|
|
||||||
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
|
||||||
constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float
|
constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float
|
||||||
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
: (strideVectorComponents == 2) ? 2 * scalarSize
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
@@ -307,21 +366,39 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4
|
return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4
|
||||||
}
|
}
|
||||||
|
|
||||||
const glslang::TType* GetUniformTType(Uint location) const {
|
// The flattened type of the uniform at `location`. This is what replaced
|
||||||
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
// GetUniformTType(): the same information, owned by the program instead of by a
|
||||||
return uniform.getType();
|
// glslang pool, so it stays valid for a link served from the L1 translation memo.
|
||||||
|
const TypeFacts& GetUniformTypeFacts(Uint location) const {
|
||||||
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); }
|
// Replaces GetUniformTType(), which used to hand a raw glslang::TType* - into a
|
||||||
|
// pool the program no longer necessarily owns - out to the DirectGLES image-format
|
||||||
|
// bake. These are the only three things any caller ever read off it.
|
||||||
|
Bool UniformHasDeclaredImageFormat(Uint location) const {
|
||||||
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.hasFormat;
|
||||||
|
}
|
||||||
|
Uint GetUniformDeclaredImageFormat(Uint location) const {
|
||||||
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.layoutFormat;
|
||||||
|
}
|
||||||
|
// Matrix column count, 0 for a non-matrix. The global-UBO fallback allocator sizes a
|
||||||
|
// matrix slot from it.
|
||||||
|
Int GetUniformMatrixColumns(Uint location) const {
|
||||||
|
const auto& uniform = UniformAt(Artifacts().uniformIndexInTProgram[location]);
|
||||||
|
return uniform.type.isMatrix ? uniform.type.matrixCols : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsUniformOpaqueAtLocation(Uint location) const {
|
||||||
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.isOpaque;
|
||||||
|
}
|
||||||
|
|
||||||
const String& GetUniformName(Uint location) const {
|
const String& GetUniformName(Uint location) const {
|
||||||
auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]);
|
return UniformAt(Artifacts().uniformIndexInTProgram[location]).name;
|
||||||
return uniform.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const String& GetActiveUniformName(Uint index) const {
|
const String& GetActiveUniformName(Uint index) const {
|
||||||
auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index));
|
return UniformAt(TProgramUniformIndex(index)).name;
|
||||||
return uniform.name;
|
|
||||||
}
|
}
|
||||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||||
@@ -355,17 +432,17 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
|
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
|
||||||
// reading or writing a whole uniform's storage - a bounds check, a copy between two
|
// reading or writing a whole uniform's storage - a bounds check, a copy between two
|
||||||
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
||||||
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
|
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) {
|
||||||
if (type != nullptr && type->isMatrix()) {
|
if (type.isMatrix) {
|
||||||
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
|
return static_cast<SizeT>(type.matrixCols) * 4 * sizeof(Float);
|
||||||
}
|
}
|
||||||
if (type != nullptr && type->getBasicType() == glslang::EbtDouble) {
|
if (type.isDouble) {
|
||||||
return tightSize / 2;
|
return tightSize / 2;
|
||||||
}
|
}
|
||||||
return tightSize;
|
return tightSize;
|
||||||
}
|
}
|
||||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||||
return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location));
|
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
|
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
|
||||||
@@ -476,14 +553,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
Uint32 GetActiveFragmentOutputLocationMask() const {
|
Uint32 GetActiveFragmentOutputLocationMask() const {
|
||||||
if (!Artifacts().program) {
|
if (Artifacts().pipeOutputReflection.empty()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 mask = 0;
|
Uint32 mask = 0;
|
||||||
const Int outputCount = Artifacts().program->getNumPipeOutputs();
|
const Int outputCount = static_cast<Int>(Artifacts().pipeOutputReflection.size());
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const Int location = static_cast<Int>(Artifacts().program->getPipeOutput(index).layoutLocation());
|
const Int location = Artifacts().pipeOutputReflection[index].location;
|
||||||
if (location >= 0 && location < 32) {
|
if (location >= 0 && location < 32) {
|
||||||
mask |= (1u << location);
|
mask |= (1u << location);
|
||||||
}
|
}
|
||||||
@@ -491,38 +568,34 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
Int GetActiveFragmentOutputCount() const {
|
Int GetActiveFragmentOutputCount() const {
|
||||||
return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0;
|
return static_cast<Int>(Artifacts().pipeOutputReflection.size());
|
||||||
}
|
}
|
||||||
const String& GetActiveFragmentOutputName(Uint index) const {
|
const String& GetActiveFragmentOutputName(Uint index) const {
|
||||||
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null");
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
|
||||||
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
|
"ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index);
|
||||||
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).name;
|
return Artifacts().pipeOutputReflection[index].name;
|
||||||
}
|
}
|
||||||
Int GetFragmentOutputLocation(Uint index) const {
|
Int GetFragmentOutputLocation(Uint index) const {
|
||||||
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null");
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
|
||||||
"ProgramObject::GetFragmentOutputLocation: index=%u out of range",
|
"ProgramObject::GetFragmentOutputLocation: index=%u out of range",
|
||||||
index);
|
index);
|
||||||
return static_cast<Int>(Artifacts().program->getPipeOutput(static_cast<Int>(index)).layoutLocation());
|
return Artifacts().pipeOutputReflection[index].location;
|
||||||
}
|
}
|
||||||
GLint GetActiveFragmentOutputArraySize(Uint index) const {
|
GLint GetActiveFragmentOutputArraySize(Uint index) const {
|
||||||
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null");
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
|
||||||
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
|
"ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index);
|
||||||
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).size;
|
return Artifacts().pipeOutputReflection[index].size;
|
||||||
}
|
}
|
||||||
GLenum GetFragmentOutputType(Uint index) const {
|
GLenum GetFragmentOutputType(Uint index) const {
|
||||||
MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null");
|
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().pipeOutputReflection.size()),
|
||||||
MOBILEGL_ASSERT(index < static_cast<Uint>(Artifacts().program->getNumPipeOutputs()),
|
|
||||||
"ProgramObject::GetFragmentOutputType: index=%u out of range",
|
"ProgramObject::GetFragmentOutputType: index=%u out of range",
|
||||||
index);
|
index);
|
||||||
return Artifacts().program->getPipeOutput(static_cast<Int>(index)).glDefineType;
|
return Artifacts().pipeOutputReflection[index].glDefineType;
|
||||||
}
|
}
|
||||||
GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
|
GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; }
|
||||||
const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
|
const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; }
|
||||||
GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).glDefineType; }
|
GLenum GetActiveAttribType(Uint index) const { return Artifacts().pipeInputReflection[index].glDefineType; }
|
||||||
GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().program->getPipeInput(static_cast<Int>(index)).size; }
|
GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().pipeInputReflection[index].size; }
|
||||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names;
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names;
|
||||||
// GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
|
// GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input
|
||||||
// resource queries enumerate builtins).
|
// resource queries enumerate builtins).
|
||||||
@@ -534,7 +607,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
const String& GetActiveAttribName(Uint index) const {
|
const String& GetActiveAttribName(Uint index) const {
|
||||||
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
return NormalizeBuiltinPipeInputName(Artifacts().pipeInputReflection[index].name);
|
||||||
}
|
}
|
||||||
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
|
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
|
||||||
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and
|
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and
|
||||||
@@ -627,7 +700,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// which means the change is only honoured by regenerating the program. That
|
// which means the change is only honoured by regenerating the program. That
|
||||||
// regeneration is gated on link-shaped versions, so without a counter that moves
|
// regeneration is gated on link-shaped versions, so without a counter that moves
|
||||||
// here the new unit would never reach the driver.
|
// here the new unit would never reach the driver.
|
||||||
if (const glslang::TType* type = GetUniformTType(location); type != nullptr && type->isImage()) {
|
if (GetUniformTypeFacts(location).isImage) {
|
||||||
++m_imageUnitVersion;
|
++m_imageUnitVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -702,20 +775,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
|
// SIGSEGV inside glslang::TProgram::getNumPipeInputs - KHR-GL30.api.coverage does exactly
|
||||||
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
|
// this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D
|
||||||
// throw ahead of it stopped killing the run first.
|
// throw ahead of it stopped killing the run first.
|
||||||
Int GetActiveAtomicCounterCount() const {
|
|
||||||
const auto& program = Artifacts().program;
|
|
||||||
return program ? program->getNumAtomicCounters() : 0;
|
|
||||||
}
|
|
||||||
Int GetActiveAttributesCount() const {
|
Int GetActiveAttributesCount() const {
|
||||||
const auto& program = Artifacts().program;
|
return static_cast<Int>(Artifacts().pipeInputReflection.size());
|
||||||
return program ? program->getNumPipeInputs() : 0;
|
|
||||||
}
|
}
|
||||||
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
// GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse
|
||||||
// materializes for default-block uniforms is filtered out by DoReflection.
|
// materializes for default-block uniforms is filtered out by DoReflection.
|
||||||
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
Int GetActiveUniformBlocksCount() const { return static_cast<Int>(Artifacts().glBlockIndexToTProgram.size()); }
|
||||||
GLuint GetComputeLocalSize(Uint dim) const {
|
GLuint GetComputeLocalSize(Uint dim) const {
|
||||||
const auto& program = Artifacts().program;
|
return dim < 3u ? Artifacts().computeLocalSize[dim] : 0u;
|
||||||
return program ? program->getLocalSize(static_cast<Int>(dim)) : 0;
|
|
||||||
}
|
}
|
||||||
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; }
|
||||||
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; }
|
||||||
@@ -739,11 +806,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
|
||||||
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
|
||||||
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
|
||||||
return (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u;
|
return (static_cast<Uint>(BlockAt(Artifacts().glBlockIndexToTProgram[index]).size) + 15u) & ~15u;
|
||||||
}
|
}
|
||||||
|
|
||||||
const String& GetUniformBlockName(Uint index) const {
|
const String& GetUniformBlockName(Uint index) const {
|
||||||
auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
|
const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]);
|
||||||
return ubo.name;
|
return ubo.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,7 +841,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
|
||||||
const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]);
|
const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]);
|
||||||
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
|
const auto stageMask = static_cast<EShLanguageMask>(1 << stage);
|
||||||
return (ubo.stages & stageMask) != 0;
|
return (ubo.stages & stageMask) != 0;
|
||||||
}
|
}
|
||||||
@@ -855,8 +922,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
|
// (MG_Impl/GLImpl/Program/ProgramInterface.cpp), which has to enumerate buffer
|
||||||
// blocks, buffer variables, atomic counters and per-stage reference masks. Null
|
// blocks, buffer variables, atomic counters and per-stage reference masks. Null
|
||||||
// until a link has succeeded. Read through the join gate like everything else.
|
// until a link has succeeded. Read through the join gate like everything else.
|
||||||
const glslang::TProgram* GetReflection() const { return Artifacts().program.get(); }
|
|
||||||
|
|
||||||
Int GetShaderIndexByStage(ShaderStage stage) const {
|
Int GetShaderIndexByStage(ShaderStage stage) const {
|
||||||
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
|
auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr<ShaderObject>& shader) {
|
||||||
return shader->GetShaderStage() == stage;
|
return shader->GetShaderStage() == stage;
|
||||||
@@ -909,9 +974,50 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// what makes "every read of link output joins the pending link" a property the
|
// what makes "every read of link output joins the pending link" a property the
|
||||||
// compiler checks rather than a review item - a new reader cannot spell the field
|
// compiler checks rather than a review item - a new reader cannot spell the field
|
||||||
// without going through the gate.
|
// without going through the gate.
|
||||||
|
// ---- the owned mirror of glslang's reflection ----
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS. Every GL query about a linked program used to be answered by
|
||||||
|
// asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix()
|
||||||
|
// and friends. That made the TProgram part of the program's PERMANENT state, which
|
||||||
|
// in turn made the whole front end (parse + link) unskippable: the L1 shader
|
||||||
|
// translation memo could hand back the SPIR-V but the reflection still had to be
|
||||||
|
// rebuilt from a freshly parsed AST.
|
||||||
|
//
|
||||||
|
// These three tables are a snapshot of everything the query surface ever reads off
|
||||||
|
// the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into
|
||||||
|
// a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection),
|
||||||
|
// they are copyable, immutable after the link, and safe to memoize and share between
|
||||||
|
// ProgramObjects and threads. Once they are filled, `program` is dead weight to
|
||||||
|
// everything except DoReflection itself.
|
||||||
|
//
|
||||||
|
// INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram,
|
||||||
|
// glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every
|
||||||
|
// accessor that used to call program->getUniform(i) indexes uniformReflection[i]
|
||||||
|
// instead, unchanged in every other respect.
|
||||||
|
|
||||||
struct LinkArtifacts {
|
struct LinkArtifacts {
|
||||||
|
// Live only between LinkProgram() and the end of DoReflection. Everything after
|
||||||
|
// that reads the owned mirror below; a link served from the L1 memo never
|
||||||
|
// constructs one at all, so this is null for such a program and MUST NOT be
|
||||||
|
// dereferenced outside DoReflection.
|
||||||
SharedPtr<glslang::TProgram> program;
|
SharedPtr<glslang::TProgram> program;
|
||||||
|
|
||||||
|
// The owned reflection snapshot. Indexed by TProgram index; see the structs above.
|
||||||
|
Vector<UniformReflection> uniformReflection;
|
||||||
|
Vector<BlockReflection> blockReflection;
|
||||||
|
Vector<PipeInputReflection> pipeInputReflection;
|
||||||
|
Vector<PipeOutputReflection> pipeOutputReflection;
|
||||||
|
// Program-level scalars glslang answers off the linked intermediates.
|
||||||
|
// Whether the program's LAST stage is the fragment stage. A color number - and so a
|
||||||
|
// color index - exists only there; a separable tess/geometry/vertex program's
|
||||||
|
// outputs are varyings and must report -1 (KHR-GL43.program_interface_query.
|
||||||
|
// separate-programs-tess-control).
|
||||||
|
Bool lastStageIsFragment = false;
|
||||||
|
Array<GLuint, 3> computeLocalSize{};
|
||||||
|
// Replaces program->getUniformIndex(name). Maps the reflected name to its
|
||||||
|
// TProgram uniform index.
|
||||||
|
UnorderedMap<String, Int> uniformIndexByName;
|
||||||
|
|
||||||
// Attributes (Vertex in)
|
// Attributes (Vertex in)
|
||||||
Vector<String> attribs;
|
Vector<String> attribs;
|
||||||
Vector<GLenum> attribTypes;
|
Vector<GLenum> attribTypes;
|
||||||
@@ -1042,6 +1148,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ordering is explicit and nothing is exempt.
|
// ordering is explicit and nothing is exempt.
|
||||||
static void ResetLinkArtifacts(LinkArtifacts& artifacts);
|
static void ResetLinkArtifacts(LinkArtifacts& artifacts);
|
||||||
|
|
||||||
|
// The owned reflection snapshot, for the program-interface query layer. Replaces
|
||||||
|
// GetReflection(), which handed out the live glslang::TProgram - the last thing that
|
||||||
|
// forced a linked program to keep its parse alive.
|
||||||
|
const LinkArtifacts& GetLinkReflection() const {
|
||||||
|
EnsureLinkJoined();
|
||||||
|
return Artifacts();
|
||||||
|
}
|
||||||
|
|
||||||
static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) {
|
static Bool IsValidUniformLocation(const LinkArtifacts& artifacts, Int location) {
|
||||||
if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false;
|
if (location < 0 || location > static_cast<Int>(artifacts.maxUniformLocation)) return false;
|
||||||
if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false;
|
if (static_cast<SizeT>(location) >= artifacts.uniformIndexInTProgram.size()) return false;
|
||||||
@@ -1057,12 +1171,24 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
|
// for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space
|
||||||
// the artifacts' uniformIndexInTProgram stores).
|
// the artifacts' uniformIndexInTProgram stores).
|
||||||
static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) {
|
static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) {
|
||||||
const auto& uniform = artifacts.program->getUniform(tIndex);
|
return UniformAtIn(artifacts, tIndex).arraySize;
|
||||||
const glslang::TType* type = uniform.getType();
|
}
|
||||||
if (type != nullptr && type->isSizedArray()) {
|
|
||||||
return type->getOuterArraySize();
|
// Bounds-checked mirror lookup. Out of range yields a default-constructed entry
|
||||||
|
// rather than UB, which is the same shape the phase-B getters use: a program whose
|
||||||
|
// reflection is missing must stay answerable, not crash the query surface.
|
||||||
|
static const UniformReflection& UniformAtIn(const LinkArtifacts& artifacts, Int tIndex) {
|
||||||
|
static const UniformReflection kEmpty;
|
||||||
|
if (tIndex < 0 || static_cast<SizeT>(tIndex) >= artifacts.uniformReflection.size()) return kEmpty;
|
||||||
|
return artifacts.uniformReflection[tIndex];
|
||||||
|
}
|
||||||
|
const UniformReflection& UniformAt(Int tIndex) const { return UniformAtIn(Artifacts(), tIndex); }
|
||||||
|
const BlockReflection& BlockAt(Int tBlockIndex) const {
|
||||||
|
static const BlockReflection kEmpty;
|
||||||
|
if (tBlockIndex < 0 || static_cast<SizeT>(tBlockIndex) >= Artifacts().blockReflection.size()) {
|
||||||
|
return kEmpty;
|
||||||
}
|
}
|
||||||
return uniform.size < 1 ? 1 : uniform.size;
|
return Artifacts().blockReflection[tBlockIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocks until a pending link has published its artifacts. Public because a few call
|
// Blocks until a pending link has published its artifacts. Public because a few call
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -95,11 +97,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// and `diagnostics`, and this node is the sole reader of the handoff.
|
// and `diagnostics`, and this node is the sole reader of the handoff.
|
||||||
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
|
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
|
||||||
const Uint externalIndex = m_phaseA->in.externalIndex;
|
const Uint externalIndex = m_phaseA->in.externalIndex;
|
||||||
if (!handoff.ready || !handoff.reflection.program) {
|
if (!handoff.ready) {
|
||||||
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
|
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
|
||||||
// Publish nothing; spirvStatus stays false.
|
// Publish nothing; spirvStatus stays false.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A TProgram is required only to GENERATE. A link served from the L1 memo has none by
|
||||||
|
// construction - that is the entire point of the widened payload - and its SPIR-V and
|
||||||
|
// routing tables arrive ready-made in cachedSpirv.
|
||||||
|
if (!handoff.cachedSpirv && !handoff.reflection.program) return;
|
||||||
|
|
||||||
|
// An L1 hit already carries everything this phase would have produced. Publish it
|
||||||
|
// and stop: no GlslangToSpv, no spirv-opt, no routing pass.
|
||||||
|
if (handoff.cachedSpirv) {
|
||||||
|
artifacts = *handoff.cachedSpirv;
|
||||||
|
MGLOG_D("ProgramObject %u: L1 cache hit - %zu SPIR-V module(s) and the global-UBO "
|
||||||
|
"routing reused",
|
||||||
|
externalIndex, artifacts.generatedSpirv.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
||||||
const Bool deferOutputValidationForDirectVulkan =
|
const Bool deferOutputValidationForDirectVulkan =
|
||||||
@@ -117,13 +133,18 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e.
|
// * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e.
|
||||||
// the 2nd..Nth link of a shared shader): freed here in full. The handoff is their
|
// the 2nd..Nth link of a shared shader): freed here in full. The handoff is their
|
||||||
// ONLY owner.
|
// ONLY owner.
|
||||||
// * CAS-WINNER shaders (the common case - one shader object linked into one
|
// * L1c-HIT shaders (the compile published a verdict and never parsed, so the parse
|
||||||
// program, which is every program of an Iris pack load): NOT freed here. The
|
// was made on demand by ClaimParsedShader): freed here in full, exactly like a
|
||||||
// winner branch returns a COPY of ShaderCompileTask::artifacts.shader
|
// CAS loser and for the same reason - the handoff is their only owner. This
|
||||||
// (ShaderCompileTask.cpp:320) and the node never releases its own reference, while
|
// category did not exist before the translation memo's compile half, and it makes
|
||||||
// phase A holds that node through in.shaders[i].compiled for its whole life - and
|
// the clear below strictly more effective than the paragraph below describes.
|
||||||
// phase A lives until PhaseAReleaser fires at the end of this body. So the
|
// * CAS-WINNER shaders (one shader object linked into one program, whose compile
|
||||||
// refcount goes 2 -> 1 here and the arena dies where it would have died anyway.
|
// MISSED L1c and therefore stored its parse): NOT freed here. The winner branch
|
||||||
|
// returns a COPY of ShaderCompileTask::artifacts.shader and the node never
|
||||||
|
// releases its own reference, while phase A holds that node through
|
||||||
|
// in.shaders[i].compiled for its whole life - and phase A lives until
|
||||||
|
// PhaseAReleaser fires at the end of this body. So the refcount goes 2 -> 1 here
|
||||||
|
// and the arena dies where it would have died anyway.
|
||||||
//
|
//
|
||||||
// Making it free the winner's arena too means releasing whatever pins the TShader
|
// Making it free the winner's arena too means releasing whatever pins the TShader
|
||||||
// inside the compile node, and neither obvious route is safe as a drive-by: moving out
|
// inside the compile node, and neither obvious route is safe as a drive-by: moving out
|
||||||
@@ -137,6 +158,23 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
|
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
|
||||||
BuildGlobalUboRouting(handoff, externalIndex);
|
BuildGlobalUboRouting(handoff, externalIndex);
|
||||||
|
|
||||||
|
// The completed front end goes into the L1 memo HERE, where both halves exist: phase
|
||||||
|
// A's LinkArtifacts (carried in the handoff) and this phase's SpirvArtifacts.
|
||||||
|
//
|
||||||
|
// Only a clean run is memoized. A failed optimizer run leaves a module as whatever the
|
||||||
|
// chain got to before it gave up, and that is exactly the binary no other program
|
||||||
|
// should ever be handed.
|
||||||
|
if (artifacts.spirvStatus && handoff.spirvCacheKey.Valid() && handoff.linkArtifactsForCache) {
|
||||||
|
auto payload = MakeShared<ProgramTranslationResult>();
|
||||||
|
payload->link = *handoff.linkArtifactsForCache;
|
||||||
|
payload->link.program.reset(); // belt and braces: never memoize a glslang arena
|
||||||
|
payload->spirv = artifacts;
|
||||||
|
const SizeT payloadBytes = ProgramTranslationResultBytes(*payload);
|
||||||
|
GetProgramTranslationCache().Insert(handoff.spirvCacheKey,
|
||||||
|
ProgramTranslationResultPtr(Move(payload)),
|
||||||
|
payloadBytes);
|
||||||
|
}
|
||||||
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
|
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
|
||||||
artifacts.generatedSpirv.size());
|
artifacts.generatedSpirv.size());
|
||||||
}
|
}
|
||||||
@@ -296,22 +334,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
|
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
|
||||||
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
||||||
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
|
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
|
||||||
const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]);
|
const auto& uniform =
|
||||||
const glslang::TType* type = uniform.getType();
|
ProgramObject::UniformAtIn(reflection, reflection.uniformIndexInTProgram[location]);
|
||||||
if (type != nullptr && type->isOpaque()) continue;
|
if (uniform.type.isOpaque) continue;
|
||||||
if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() &&
|
// Member of a named uniform block: not settable through glUniform*, so it needs
|
||||||
std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(),
|
// no global-UBO shadow storage. tProgramBlockIndexToGl[i] >= 0 means block i is
|
||||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
// GL-visible, i.e. NOT the synthesized MGL_GLOBAL_UBO - which is exactly what the
|
||||||
// Member of a named uniform block: not settable through glUniform*, so it
|
// strstr(GLOBAL_UBO_NAME) test this replaced was asking, without needing the
|
||||||
// needs no global-UBO shadow storage.
|
// TProgram to spell the block name.
|
||||||
|
if (uniform.index >= 0 &&
|
||||||
|
uniform.index < static_cast<Int>(reflection.tProgramBlockIndexToGl.size()) &&
|
||||||
|
reflection.tProgramBlockIndexToGl[uniform.index] >= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// std140-style slot: the matrix upload paths write column vectors at
|
// std140-style slot: the matrix upload paths write column vectors at
|
||||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||||
if (type != nullptr && type->isMatrix()) {
|
if (uniform.type.isMatrix) {
|
||||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
slotSize = static_cast<SizeT>(uniform.type.matrixCols) * 16u;
|
||||||
}
|
}
|
||||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||||
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "ProgramTranslationCache.h"
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
namespace {
|
||||||
|
// ---- L1 caps: 48 entries / 24 MiB ----
|
||||||
|
//
|
||||||
|
// Both numbers moved when the payload grew from "the SPIR-V modules" to "the whole
|
||||||
|
// front end". An entry is now the stages' preprocessed source (the key), the SPIR-V,
|
||||||
|
// the reflection snapshot and the global-UBO shadow - roughly twice what it was - so
|
||||||
|
// the byte budget doubled and the entry count came down to keep the worst case in the
|
||||||
|
// same place on a phone.
|
||||||
|
//
|
||||||
|
// The shape of the choice has not changed: this cache exists for REPETITION, not
|
||||||
|
// coverage. A KHR-GL33.texture_swizzle smoke case builds 2592 programs out of fewer
|
||||||
|
// than ten distinct ones, so a handful of entries serves it completely; an Iris
|
||||||
|
// shaderpack load is ~300-600 MOSTLY DISTINCT programs that would never hit however
|
||||||
|
// large the cache is, so a bigger cap there buys nothing and costs resident memory.
|
||||||
|
// 48 is comfortably above the distinct-program count of every repetition workload
|
||||||
|
// measured, and 24 MiB bounds the pathological case - a pack whose ~100 KB stages
|
||||||
|
// really are re-linked - at roughly three times the existing 8 MiB
|
||||||
|
// ShaderPreprocessCache budget, which is the other memo on this path.
|
||||||
|
constexpr SizeT kMaxEntries = 48;
|
||||||
|
constexpr SizeT kMaxBytes = 24u * 1024u * 1024u;
|
||||||
|
|
||||||
|
SizeT StringsBytes(const Vector<String>& values) {
|
||||||
|
SizeT bytes = 0;
|
||||||
|
for (const String& value : values) bytes += value.size() + sizeof(String);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT ResourcesBytes(const Vector<ProgramObject::ResourceReflection>& records) {
|
||||||
|
SizeT bytes = records.size() * sizeof(ProgramObject::ResourceReflection);
|
||||||
|
for (const auto& record : records) bytes += record.name.size();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Approximate on purpose: it feeds a budget, not an allocator. It counts the things that
|
||||||
|
// actually scale with shader size - the SPIR-V, the reflection names, the UBO shadow -
|
||||||
|
// and ignores per-entry fixed overhead.
|
||||||
|
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result) {
|
||||||
|
SizeT bytes = 0;
|
||||||
|
for (const auto& module : result.spirv.generatedSpirv) bytes += module.size() * sizeof(unsigned);
|
||||||
|
bytes += result.spirv.uniformOffsets.size() * sizeof(Uint);
|
||||||
|
bytes += result.spirv.globalUboScratch.size();
|
||||||
|
bytes += ResourcesBytes(result.link.uniformReflection);
|
||||||
|
bytes += ResourcesBytes(result.link.blockReflection);
|
||||||
|
bytes += ResourcesBytes(result.link.pipeInputReflection);
|
||||||
|
bytes += ResourcesBytes(result.link.pipeOutputReflection);
|
||||||
|
bytes += StringsBytes(result.link.attribs);
|
||||||
|
bytes += StringsBytes(result.link.xfbInterfaceNames);
|
||||||
|
bytes += result.link.infoLog.size();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
|
||||||
|
GetProgramTranslationCache() {
|
||||||
|
// DELIBERATELY LEAKED - see the same note on the L2 cache in
|
||||||
|
// MG_Util/ShaderTranspiler/TranslationCache.cpp. A function-local static OBJECT
|
||||||
|
// registers its destructor at first use, and first use here is a ShaderCompilePool
|
||||||
|
// worker; ShaderCompilePool's own atexit drain sentinel is registered strictly
|
||||||
|
// earlier, and exit handlers run in reverse order - so the cache would be destroyed
|
||||||
|
// while workers were still inserting into it. A function-local static POINTER is
|
||||||
|
// trivially destructible and registers no exit handler at all.
|
||||||
|
static auto* const kCache =
|
||||||
|
new MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>(
|
||||||
|
"ShaderTranslationCache L1 (GLSL->front end)", kMaxEntries, kMaxBytes);
|
||||||
|
return *kCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearProgramTranslationCache() { GetProgramTranslationCache().Clear(); }
|
||||||
|
|
||||||
|
void LogProgramTranslationCacheStats() { GetProgramTranslationCache().LogStats(); }
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.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 <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// ===================================================================================
|
||||||
|
// L1 of the shader translation memo: THE WHOLE FRONT END of one glLinkProgram.
|
||||||
|
//
|
||||||
|
// A hit skips the glslang link and mapIO, GlslangToSpv, the 11-pass
|
||||||
|
// SanitizeAndOptimizeBinary chain, buildReflection, and the global-UBO routing pass. No
|
||||||
|
// TProgram is constructed at all - which is only possible because the GL query surface no
|
||||||
|
// longer reads one (see ProgramObject::UniformReflection and
|
||||||
|
// ProgramLinkTask::SnapshotGlslangReflection).
|
||||||
|
//
|
||||||
|
// IT DOES NOT SKIP THE PARSE, and no widening of this payload could: the parse belongs to
|
||||||
|
// glCompileShader, a different entry point one job earlier, and it has already run by the
|
||||||
|
// time a link looks this key up. Skipping it is L1c's job - the compile half of the memo,
|
||||||
|
// in MG_Util/ShaderTranspiler/TranslationCache.h. The two together are what make a
|
||||||
|
// repeated program build construct no glslang object of any kind; either one alone leaves
|
||||||
|
// roughly half the front end on the hot path (~322 us of parse against a ~650 us
|
||||||
|
// CTS-shaped program build, and 1.45-1.48x measured on device with L1 alone).
|
||||||
|
//
|
||||||
|
// WHY THE PAYLOAD IS THE WHOLE THING rather than just the SPIR-V: the frontend answers
|
||||||
|
// glGetActiveUniform, glGetProgramResource*, glGetUniformLocation and the rest out of
|
||||||
|
// LinkArtifacts, and glUniform*/glGetUniform* out of SpirvArtifacts. Caching only the
|
||||||
|
// modules would have left the link on the hot path to rebuild exactly the data the
|
||||||
|
// payload can carry.
|
||||||
|
//
|
||||||
|
// WHY IT LIVES HERE AND NOT IN MG_Util: the payload is a ProgramObject::LinkArtifacts
|
||||||
|
// plus a ProgramObject::SpirvArtifacts, and MG_Util must not depend on MG_State. The
|
||||||
|
// KEY is plain bytes and stays in MG_Util (BuildSpirvTranslationKey), so both layers
|
||||||
|
// agree on exactly one definition of "the same front-end input".
|
||||||
|
//
|
||||||
|
// EVERYTHING IN THE PAYLOAD IS PLAIN OWNED DATA. `link.program` is null by construction:
|
||||||
|
// the whole point is that a hit never has a glslang arena to point into. Both structs
|
||||||
|
// were audited field by field - the only member that ever pointed into glslang-owned
|
||||||
|
// memory was `program` itself, and TUniformInitializer / XfbVarying, which look like
|
||||||
|
// glslang types, are std::string + std::vector aggregates.
|
||||||
|
struct ProgramTranslationResult {
|
||||||
|
// program == nullptr, always. Asserted at insert.
|
||||||
|
ProgramObject::LinkArtifacts link;
|
||||||
|
ProgramObject::SpirvArtifacts spirv;
|
||||||
|
};
|
||||||
|
using ProgramTranslationResultPtr = SharedPtr<const ProgramTranslationResult>;
|
||||||
|
|
||||||
|
SizeT ProgramTranslationResultBytes(const ProgramTranslationResult& result);
|
||||||
|
|
||||||
|
// Process-global, and safe to be: the FRONT-END environment fingerprint is in the key
|
||||||
|
// (see CompileEnv::frontendFingerprint), so a program built under one context's glslang
|
||||||
|
// limits can never be handed to a context with different ones - while two contexts on
|
||||||
|
// DIFFERENT GPUs that agree on those limits deliberately share entries.
|
||||||
|
//
|
||||||
|
// Global rather than per-context because the producer runs on a ShaderCompilePool worker
|
||||||
|
// and must not reach MG_State::pGLContext.
|
||||||
|
MG_Util::ShaderTranspiler::BoundedTranslationCache<ProgramTranslationResult>&
|
||||||
|
GetProgramTranslationCache();
|
||||||
|
|
||||||
|
void ClearProgramTranslationCache();
|
||||||
|
void LogProgramTranslationCacheStats();
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -8,13 +8,16 @@
|
|||||||
|
|
||||||
#include "ShaderCompileTask.h"
|
#include "ShaderCompileTask.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||||
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
#include <glslang/Include/PoolAlloc.h>
|
#include <glslang/Include/PoolAlloc.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -137,8 +140,21 @@ namespace {
|
|||||||
return std::nullopt;
|
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.
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
// The half of a compile that depends on nothing but the source text, the stage and the
|
// The half of a compile that depends on nothing but the source text, the stage and the
|
||||||
// environment snapshot: preprocessing, the two lexical rejections, and the two lexical
|
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
|
||||||
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
||||||
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
||||||
// Deliberately free of any per-object state so the memo is sound.
|
// Deliberately free of any per-object state so the memo is sound.
|
||||||
@@ -172,6 +188,13 @@ namespace {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (const std::optional<String> bindingError = FindShaderStorageBindingViolation(
|
||||||
|
result.preprocessedSource, MaxShaderStorageBufferBindings(env))) {
|
||||||
|
result.outcome = ShaderPreprocessOutcome::ResourceBindingRejected;
|
||||||
|
result.infoLog = *bindingError;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
|
||||||
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
|
||||||
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
// what the backends' SPIR-V is generated from - there is no second, GL-client
|
||||||
@@ -248,17 +271,80 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(stage),
|
const GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(stage);
|
||||||
.sourceStr = shared.preprocessedSource,
|
// Always 0 on both production parse paths; see the key inventory on
|
||||||
.flags = 0,
|
// ShaderParseVerdictKeyInputs for why it is in the key regardless.
|
||||||
.env = &compileEnv};
|
constexpr Uint32 kShaderCompileFlags = 0;
|
||||||
|
|
||||||
auto result = ShaderCompiler::CompileShader(attrib);
|
// ---- L1c of the shader translation memo: the PARSE VERDICT ----------------------
|
||||||
if (result) {
|
// Everything below this probe - the glslang parse itself - is what a hit skips. What
|
||||||
|
// a hit does NOT produce is a TShader, and that is deliberate rather than a
|
||||||
|
// limitation: the TShader is consume-once, so it could never have been shared, and
|
||||||
|
// nothing on the COMPILE side of GL reads it. GL_COMPILE_STATUS, the info log,
|
||||||
|
// GL_SHADER_SOURCE, attach/detach and reuse across programs are all answered from
|
||||||
|
// what the verdict and the source-only half already carry.
|
||||||
|
//
|
||||||
|
// The parse is not skipped, it is DEFERRED: ClaimParsedShader re-parses on demand
|
||||||
|
// when a link finds no stored parse. A link that hits L1 never asks, so the parse
|
||||||
|
// never happens at all; a link that misses pays exactly one parse, where the CAS
|
||||||
|
// loser has always paid it. See TranslationCache.h's L1c section.
|
||||||
|
const TranslationCacheKey parseKey =
|
||||||
|
ShaderTranslationCacheEnabled()
|
||||||
|
? BuildShaderParseVerdictKey(ShaderParseVerdictKeyInputs{
|
||||||
|
.frontendFingerprint = compileEnv.frontendFingerprint,
|
||||||
|
.shaderType = glShaderType,
|
||||||
|
.preprocessedSource = StringView(shared.preprocessedSource),
|
||||||
|
.shaderCompileFlags = kShaderCompileFlags})
|
||||||
|
: TranslationCacheKey{};
|
||||||
|
const ShaderParseVerdictPtr verdict =
|
||||||
|
parseKey.Valid() ? GetShaderParseVerdictCache().Find(parseKey) : nullptr;
|
||||||
|
|
||||||
|
// The two branches produce exactly one thing between them - a verdict, plus a TShader
|
||||||
|
// only when this task actually parsed - and converge on one publish below. Keeping the
|
||||||
|
// publish common is what stops a hit and a miss from ever drifting on WHAT a compile
|
||||||
|
// makes observable.
|
||||||
|
Bool parsedOk = false;
|
||||||
|
String parseLog;
|
||||||
|
SharedPtr<glslang::TShader> parsedShader;
|
||||||
|
|
||||||
|
if (verdict) {
|
||||||
|
parsedOk = verdict->parsed;
|
||||||
|
parseLog = verdict->infoLog;
|
||||||
|
MGLOG_D("ShaderCompileTask: shader %u (stage %d) L1c hit - the glslang parse was skipped; "
|
||||||
|
"compileStatus = %d",
|
||||||
|
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
|
||||||
|
} else {
|
||||||
|
const ShaderAttrib attrib{.shaderType = glShaderType,
|
||||||
|
.sourceStr = shared.preprocessedSource,
|
||||||
|
.flags = kShaderCompileFlags,
|
||||||
|
.env = &compileEnv};
|
||||||
|
auto result = ShaderCompiler::CompileShader(attrib);
|
||||||
|
parsedOk = result.has_value();
|
||||||
|
if (parsedOk) {
|
||||||
|
parsedShader = result.value();
|
||||||
|
} else {
|
||||||
|
parseLog = result.error().log;
|
||||||
|
}
|
||||||
|
if (parseKey.Valid()) {
|
||||||
|
auto freshVerdict = MakeShared<ShaderParseVerdict>();
|
||||||
|
freshVerdict->parsed = parsedOk;
|
||||||
|
// Empty on success by construction, matching what the publish below does with
|
||||||
|
// the artifacts' own log; the diagnostic the application reads on failure.
|
||||||
|
freshVerdict->infoLog = parseLog;
|
||||||
|
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
|
||||||
|
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
|
||||||
|
verdictBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedOk) {
|
||||||
artifacts.compileStatus = true;
|
artifacts.compileStatus = true;
|
||||||
artifacts.shader = result.value();
|
// NULL ON AN L1c HIT, and that is a supported state rather than an oversight: see
|
||||||
|
// ShaderCompileArtifacts::shader and ClaimParsedShader.
|
||||||
|
artifacts.shader = Move(parsedShader);
|
||||||
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and
|
// Copy, not move: `shared` may alias a cache entry that has to outlive us, and
|
||||||
// `fresh` is about to be handed to the cache.
|
// `fresh` is about to be handed to the cache. Populated on the hit path too - it
|
||||||
|
// is what ClaimParsedShader's deferred parse consumes.
|
||||||
artifacts.preprocessedSource = shared.preprocessedSource;
|
artifacts.preprocessedSource = shared.preprocessedSource;
|
||||||
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
||||||
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
||||||
@@ -267,7 +353,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
artifacts.infoLog = result.error().log;
|
artifacts.infoLog = Move(parseLog);
|
||||||
// Deferred, not logged here, for two reasons. MGLOG from a pool thread interleaves
|
// Deferred, not logged here, for two reasons. MGLOG from a pool thread interleaves
|
||||||
// mid-line with the GL thread's own output and lands out of order relative to the
|
// mid-line with the GL thread's own output and lands out of order relative to the
|
||||||
// glCompileShader that caused it; diagnostics.logLines is replayed by the join, on
|
// glCompileShader that caused it; diagnostics.logLines is replayed by the join, on
|
||||||
@@ -310,10 +396,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either another link already consumed the stored parse (and mapIO mutated its
|
// Three ways to be here: another link already consumed the stored parse (and mapIO
|
||||||
// intermediate), or there never was one. Re-parse the preprocessed source through the
|
// mutated its intermediate); the compile hit L1c and never parsed at all; or there
|
||||||
// identical configuration; that costs one glslang parse, which is what GenerateBinary
|
// simply never was one. All three want the same thing - parse the preprocessed source
|
||||||
// used to spend here on EVERY link rather than only on reuse.
|
// through the identical configuration. That costs one glslang parse, which is what
|
||||||
|
// GenerateBinary used to spend here on EVERY link rather than only when needed.
|
||||||
//
|
//
|
||||||
// The guard is not optional on this path: from stage 4 this runs on a pool worker,
|
// The guard is not optional on this path: from stage 4 this runs on a pool worker,
|
||||||
// and TShader::parse would leave that worker's TLS allocator pointing at a pool the
|
// and TShader::parse would leave that worker's TLS allocator pointing at a pool the
|
||||||
@@ -329,7 +416,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
.env = artifacts.env.get()};
|
.env = artifacts.env.get()};
|
||||||
auto result = ShaderCompiler::CompileShader(attrib);
|
auto result = ShaderCompiler::CompileShader(attrib);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// Should be unreachable: the same source parsed successfully at Compile().
|
// Should be unreachable. This exact (stage, preprocessed source, front-end env)
|
||||||
|
// parsed successfully once - either at this node's own Compile(), or at the
|
||||||
|
// Compile() whose verdict L1c handed this node - and every input the parse reads
|
||||||
|
// is covered by that tuple. ConsumeShaders turns a null into a failed link with a
|
||||||
|
// named internal error rather than a crash, which is the right shape for a
|
||||||
|
// "cannot happen" that would otherwise be a silent miscompile.
|
||||||
outReparseLog = result.error().log;
|
outReparseLog = result.error().log;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// re-parse in ClaimParsedShader() reproduces the original parse exactly, instead of
|
// re-parse in ClaimParsedShader() reproduces the original parse exactly, instead of
|
||||||
// re-reading whatever the backend says now.
|
// re-reading whatever the backend says now.
|
||||||
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
SharedPtr<const MG_Util::ShaderTranspiler::CompileEnv> env;
|
||||||
|
// The parse, WHEN THIS COMPILE ACTUALLY PARSED - and null otherwise, including when
|
||||||
|
// compileStatus is true.
|
||||||
|
//
|
||||||
|
// That combination is not a half-finished compile; it is an L1c hit. The translation
|
||||||
|
// memo's compile half (TranslationCache.h) knows this exact (stage, preprocessed
|
||||||
|
// source, front-end env) parses cleanly, so the verdict is published without running
|
||||||
|
// glslang. What a hit cannot hand over is the TShader itself: mapIO mutates its
|
||||||
|
// aliased intermediate at link, so a parse feeds exactly ONE link and could never
|
||||||
|
// have been shared between compiles.
|
||||||
|
//
|
||||||
|
// Nothing on the compile side of GL reads this - GL_COMPILE_STATUS, the info log,
|
||||||
|
// GL_SHADER_SOURCE, attach/detach and reuse across programs are all answered from the
|
||||||
|
// fields below. The one reader is ClaimParsedShader, which treats null as "parse it
|
||||||
|
// now", which is the same path the consume-once CAS loser has always taken.
|
||||||
SharedPtr<glslang::TShader> shader;
|
SharedPtr<glslang::TShader> shader;
|
||||||
// The source the parse actually consumed (after PreprocessShaderSource), kept for
|
// The source the parse actually consumed (after PreprocessShaderSource), kept for
|
||||||
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
|
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
|
||||||
@@ -53,8 +67,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
|
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
|
||||||
// work - preprocess, the two lexical rejections, the two lexical extractions, and the
|
// work - preprocess, the two lexical rejections, the two lexical extractions, and (unless
|
||||||
// glslang parse - with every input it needs owned by the node itself.
|
// the translation memo's compile half already knows the answer) the glslang parse - with
|
||||||
|
// every input it needs owned by the node itself.
|
||||||
//
|
//
|
||||||
// That ownership is the whole point. The node reads no GL-thread state (the source is a
|
// That ownership is the whole point. The node reads no GL-thread state (the source is a
|
||||||
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
|
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
|
||||||
@@ -87,22 +102,29 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
||||||
ShaderCompileArtifacts artifacts;
|
ShaderCompileArtifacts artifacts;
|
||||||
|
|
||||||
// Hands out a link-consumable TShader, exactly once for the stored parse.
|
// Hands out a link-consumable TShader, parsing one on demand when this node has none.
|
||||||
//
|
//
|
||||||
// glslang's mapIO mutates the TShader's aliased intermediate, so the parse this node
|
// TWO WAYS TO GET HERE WITHOUT A STORED PARSE, and they share one implementation:
|
||||||
// produced may feed exactly ONE link; every later link (a relink, or the same shader
|
// * the CAS loser. glslang's mapIO mutates the TShader's aliased intermediate, so
|
||||||
// attached to a second program) needs a fresh parse. The claim is a CAS on this
|
// the parse this node produced may feed exactly ONE link; every later link (a
|
||||||
// shared node rather than a flag on the ShaderObject because from stage 4 the two
|
// relink, or the same shader attached to a second program) needs a fresh one. The
|
||||||
// callers can be two ProgramLinkTasks running on two workers: two programs sharing
|
// claim is a CAS on this shared node rather than a flag on the ShaderObject
|
||||||
// one shader, linked back to back. Copying the parse out and tracking consumed-ness
|
// because from stage 4 the two callers can be two ProgramLinkTasks on two
|
||||||
// per program would let both of them decide they were the first, run mapIO over the
|
// workers: two programs sharing one shader, linked back to back. Copying the
|
||||||
// same intermediate twice, and ship silently corrupt SPIR-V.
|
// parse out and tracking consumed-ness per program would let both of them decide
|
||||||
|
// they were the first, run mapIO over the same intermediate twice, and ship
|
||||||
|
// silently corrupt SPIR-V.
|
||||||
|
// * an L1c HIT. The compile published a verdict without parsing at all (see
|
||||||
|
// ShaderCompileArtifacts::shader), so this call IS the parse - deferred out of
|
||||||
|
// glCompileShader to the first link that genuinely needs an AST. A link served
|
||||||
|
// from L1 never gets here, which is the whole point: that program's front end
|
||||||
|
// never constructs a glslang object of any kind.
|
||||||
//
|
//
|
||||||
// The CAS loser re-parses artifacts.preprocessedSource against THIS node's own
|
// Either way the parse runs over artifacts.preprocessedSource against THIS node's own
|
||||||
// CompileEnv (not against whatever the backend reports now), through the identical
|
// CompileEnv (not against whatever the backend reports now), through the identical
|
||||||
// CompileShader path - so winner and loser produce byte-identical SPIR-V. Callable
|
// CompileShader path - so every claimant produces byte-identical SPIR-V. Callable
|
||||||
// only once IsComplete() and compileStatus are true. Returns null only if that
|
// only once IsComplete() and compileStatus are true. Returns null only if that parse
|
||||||
// re-parse fails, and outReparseLog then carries its diagnostics.
|
// fails, and outReparseLog then carries its diagnostics.
|
||||||
//
|
//
|
||||||
// Const because the claim is the node's own synchronization, not a mutation of its
|
// Const because the claim is the node's own synchronization, not a mutation of its
|
||||||
// published artifacts: a claim that is taken and then abandoned (its link was
|
// published artifacts: a claim that is taken and then abandoned (its link was
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
ComputeLocalSizeRejected,
|
ComputeLocalSizeRejected,
|
||||||
// FindReservedIdentifierViolation rejected it.
|
// FindReservedIdentifierViolation rejected it.
|
||||||
ReservedIdentifierRejected,
|
ReservedIdentifierRejected,
|
||||||
|
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
|
||||||
|
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
||||||
|
ResourceBindingRejected,
|
||||||
// The source-only half was clean but glslang rejected the preprocessed source.
|
// The source-only half was clean but glslang rejected the preprocessed source.
|
||||||
// Memoizing this saves the parse itself on every later object with that source.
|
// Memoizing this saves the parse itself on every later object with that source.
|
||||||
ParseFailed,
|
ParseFailed,
|
||||||
|
|||||||
@@ -250,6 +250,10 @@ namespace MobileGL {
|
|||||||
return m_contentVersion;
|
return m_contentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint64 TextureObjectBase::GetShapeVersion() const {
|
||||||
|
return m_shapeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
|
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
|
||||||
const int slot = mipmapped ? 1 : 0;
|
const int slot = mipmapped ? 1 : 0;
|
||||||
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
|
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
|
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
|
||||||
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
|
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
|
||||||
virtual Uint64 GetContentVersion() const = 0;
|
virtual Uint64 GetContentVersion() const = 0;
|
||||||
|
// Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level
|
||||||
|
// set, the internal format, the level range (see BumpShapeVersion). Disjoint from the
|
||||||
|
// content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size
|
||||||
|
// without dirtying a single texel, so a backend that keys its "nothing changed since
|
||||||
|
// the last sync" skip on content alone keeps a resource of the OLD size alive.
|
||||||
|
virtual Uint64 GetShapeVersion() const = 0;
|
||||||
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
|
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
|
||||||
// property of the texture's SHAPE - level sizes, level count, level range,
|
// property of the texture's SHAPE - level sizes, level count, level range,
|
||||||
// internal format - and never of its texel content, but every draw asks about
|
// internal format - and never of its texel content, but every draw asks about
|
||||||
@@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void SetImmutableLevels(Uint levels) override;
|
void SetImmutableLevels(Uint levels) override;
|
||||||
Uint16 GetTextureParamsVersion() const override;
|
Uint16 GetTextureParamsVersion() const override;
|
||||||
Uint64 GetContentVersion() const override;
|
Uint64 GetContentVersion() const override;
|
||||||
|
Uint64 GetShapeVersion() const override;
|
||||||
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
|
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
|
||||||
// Bumps the content version without touching per-level storage-dirty flags. Used when the
|
// Bumps the content version without touching per-level storage-dirty flags. Used when the
|
||||||
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
|
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
|
||||||
|
|||||||
@@ -59,9 +59,11 @@ void main()
|
|||||||
const String out = SplitReadWriteImageUniforms(source);
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
|
||||||
// Both halves: same binding, same format, same type - which is what makes two image
|
// Both halves: same binding, same format, same type - which is what makes two image
|
||||||
// variables on one image unit legal.
|
// variables on one image unit legal - and both `coherent`, which is what makes the store
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform readonly highp image2D goku;"));
|
// through one of them visible to the load through the other.
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||||
|
EXPECT_TRUE(Contains(
|
||||||
|
out, "layout(binding = 2, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||||
|
|
||||||
// The load keeps the original name, the store moves to the writeonly half.
|
// The load keeps the original name, the store moves to the writeonly half.
|
||||||
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
EXPECT_TRUE(Contains(out, "imageLoad(goku,"));
|
||||||
@@ -152,9 +154,9 @@ void main()
|
|||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
const String out = SplitReadWriteImageUniforms(source);
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform readonly highp image2D gohan[3];"));
|
EXPECT_TRUE(Contains(out, "layout(binding = 6, rgba8) uniform coherent readonly highp image2D gohan[3];"));
|
||||||
EXPECT_TRUE(Contains(out,
|
EXPECT_TRUE(Contains(
|
||||||
"layout(binding = 6, rgba8) uniform writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
out, "layout(binding = 6, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("gohan") + "[3];"));
|
||||||
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("gohan") + "[1],"));
|
||||||
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
EXPECT_TRUE(Contains(out, "imageLoad(gohan[2],"));
|
||||||
}
|
}
|
||||||
@@ -174,9 +176,11 @@ void main()
|
|||||||
)";
|
)";
|
||||||
const String out = SplitReadWriteImageUniforms(source);
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
|
||||||
// goku is read+write -> split; goku_hd is write-only -> qualified in place, not split.
|
// goku is read+write -> split (and coherent with it); goku_hd is write-only -> qualified in
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform readonly highp image2D goku;"));
|
// place, not split, and left non-coherent because nothing aliases it.
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " + WriteAlias("goku") + ";"));
|
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform coherent readonly highp image2D goku;"));
|
||||||
|
EXPECT_TRUE(Contains(
|
||||||
|
out, "layout(binding = 1, rgba8) uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";"));
|
||||||
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
EXPECT_TRUE(Contains(out, "layout(binding = 2, rgba8) uniform writeonly highp image2D goku_hd;"));
|
||||||
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
EXPECT_TRUE(Contains(out, "imageStore(goku_hd,"));
|
||||||
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
EXPECT_FALSE(Contains(out, WriteAlias("goku") + "_hd"));
|
||||||
@@ -197,6 +201,36 @@ void main()
|
|||||||
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
EXPECT_TRUE(Contains(out, "uniform readonly coherent restrict highp image2D goku;"));
|
||||||
EXPECT_TRUE(
|
EXPECT_TRUE(
|
||||||
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
Contains(out, "uniform writeonly coherent restrict highp image2D " + WriteAlias("goku") + ";"));
|
||||||
|
// ...and the coherent the split adds is not a SECOND one: a repeated memory qualifier is a
|
||||||
|
// compile error in ESSL, so the source's own has to be recognized.
|
||||||
|
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The visibility half of the split, and the reason it is not cosmetic: GLSL orders a
|
||||||
|
// same-variable read-after-write within one invocation by construction, but once the store goes
|
||||||
|
// through `mg_imageWrite_goku` and the load through `goku` the two are DIFFERENT variables, and
|
||||||
|
// the ordering only holds if both are coherent. Desktop sources almost never say so - they had
|
||||||
|
// no reason to - which is how KHR-GL4x.shader_image_load_store.advanced-memory-order's
|
||||||
|
// store/load/compare loop started reading back the value it had not stored yet.
|
||||||
|
TEST(SplitReadWriteImageUniformsTest, SplitPairIsMadeCoherentEvenWhenTheSourceIsNot) {
|
||||||
|
const String source = R"(#version 320 es
|
||||||
|
layout(binding = 2, rgba8) uniform highp image2D goku;
|
||||||
|
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
|
||||||
|
layout(location = 0) out highp vec4 mg_FragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
imageStore(goku, ivec2(0), vec4(1.0));
|
||||||
|
mg_FragColor = imageLoad(goku, ivec2(0));
|
||||||
|
imageStore(storeOnly, ivec2(0), vec4(2.0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const String out = SplitReadWriteImageUniforms(source);
|
||||||
|
EXPECT_TRUE(Contains(out, "uniform coherent readonly highp image2D goku;")) << out;
|
||||||
|
EXPECT_TRUE(Contains(out, "uniform coherent writeonly highp image2D " + WriteAlias("goku") + ";")) << out;
|
||||||
|
// Exactly the two halves of the pair, and nothing else: the store-only image is repaired in
|
||||||
|
// place, has no alias to stay visible to, and must not pay for uncached access.
|
||||||
|
EXPECT_EQ(CountOf(out, "coherent"), 2u);
|
||||||
|
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
|
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
|
||||||
|
|||||||
@@ -45,6 +45,31 @@ namespace {
|
|||||||
GLint maxFragmentSsboBlocks = 9;
|
GLint maxFragmentSsboBlocks = 9;
|
||||||
bool tessAndGeometrySsboBlocksQueried = false;
|
bool tessAndGeometrySsboBlocksQueried = false;
|
||||||
bool perStageSsboBlockQueryRaisesError = false;
|
bool perStageSsboBlockQueryRaisesError = false;
|
||||||
|
// GL_MAX_CLIP_DISTANCES. Not ES core in any version - it exists only as
|
||||||
|
// GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance - so asking a driver without
|
||||||
|
// the extension raises GL_INVALID_ENUM and leaves the out-param untouched. The "queried"
|
||||||
|
// flag is what pins the gating; the "raises error" knob is what pins the drain.
|
||||||
|
GLint maxClipDistances = 8;
|
||||||
|
bool maxClipDistancesQueried = false;
|
||||||
|
bool clipDistanceQueryRaisesError = false;
|
||||||
|
// GL_MAX_VIEWPORTS / GL_VIEWPORT_SUBPIXEL_BITS / GL_VIEWPORT_BOUNDS_RANGE are
|
||||||
|
// GL_OES_viewport_array state and, like the clip-distance pname, exist nowhere in ES core.
|
||||||
|
GLint maxViewports = 32;
|
||||||
|
GLint viewportSubpixelBits = 8;
|
||||||
|
bool viewportArrayLimitsQueried = false;
|
||||||
|
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core; GL_VIEWPORT_INDEX_PROVOKING_VERTEX comes with
|
||||||
|
// GL_OES_viewport_array. Both must go unasked where they do not exist, and a driver answer
|
||||||
|
// outside the four legal conventions must not be forwarded as one.
|
||||||
|
GLint layerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
|
||||||
|
GLint viewportIndexProvokingVertex = GL_LAST_VERTEX_CONVENTION;
|
||||||
|
bool layerProvokingVertexQueried = false;
|
||||||
|
// A driver rejecting one of the UNCONDITIONAL probes. GL_SMOOTH_LINE_WIDTH_RANGE is the
|
||||||
|
// realistic one - it is desktop-only state that every GLES driver refuses - and it stands
|
||||||
|
// in for the whole run: whatever it leaves behind must not reach the application.
|
||||||
|
bool smoothLineWidthQueryRaisesError = false;
|
||||||
|
// What the driver answers for the four multisample ceilings. Zero is the value that has
|
||||||
|
// to be floored away: the frontend would otherwise advertise a sample count it rejects.
|
||||||
|
GLint multisampleCeiling = 4;
|
||||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||||
GLint fragmentInterpolationOffsetBits = 6;
|
GLint fragmentInterpolationOffsetBits = 6;
|
||||||
@@ -160,6 +185,37 @@ namespace {
|
|||||||
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
|
||||||
*data = g_fake.maxComputeImageUniforms;
|
*data = g_fake.maxComputeImageUniforms;
|
||||||
break;
|
break;
|
||||||
|
case GL_MAX_CLIP_DISTANCES:
|
||||||
|
g_fake.maxClipDistancesQueried = true;
|
||||||
|
if (g_fake.clipDistanceQueryRaisesError) {
|
||||||
|
g_fake.pendingError = GL_INVALID_ENUM;
|
||||||
|
} else {
|
||||||
|
*data = g_fake.maxClipDistances;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GL_MAX_VIEWPORTS:
|
||||||
|
g_fake.viewportArrayLimitsQueried = true;
|
||||||
|
*data = g_fake.maxViewports;
|
||||||
|
break;
|
||||||
|
case GL_VIEWPORT_SUBPIXEL_BITS:
|
||||||
|
g_fake.viewportArrayLimitsQueried = true;
|
||||||
|
*data = g_fake.viewportSubpixelBits;
|
||||||
|
break;
|
||||||
|
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
||||||
|
g_fake.viewportArrayLimitsQueried = true;
|
||||||
|
*data = g_fake.viewportIndexProvokingVertex;
|
||||||
|
break;
|
||||||
|
case GL_LAYER_PROVOKING_VERTEX:
|
||||||
|
g_fake.layerProvokingVertexQueried = true;
|
||||||
|
*data = g_fake.layerProvokingVertex;
|
||||||
|
break;
|
||||||
|
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||||
|
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||||
|
case GL_MAX_FRAMEBUFFER_SAMPLES:
|
||||||
|
case GL_MAX_INTEGER_SAMPLES:
|
||||||
|
case GL_MAX_SAMPLES:
|
||||||
|
*data = g_fake.multisampleCeiling;
|
||||||
|
break;
|
||||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
|
||||||
g_fake.fragmentInterpolationLimitsQueried = true;
|
g_fake.fragmentInterpolationLimitsQueried = true;
|
||||||
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
if (g_fake.fragmentInterpolationQueryRaisesError) {
|
||||||
@@ -239,11 +295,22 @@ namespace {
|
|||||||
data[0] = g_fake.maxFragmentInterpolationOffset;
|
data[0] = g_fake.maxFragmentInterpolationOffset;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
||||||
|
if (g_fake.smoothLineWidthQueryRaisesError) {
|
||||||
|
g_fake.pendingError = GL_INVALID_ENUM;
|
||||||
|
} else {
|
||||||
|
data[0] = 0.0f;
|
||||||
|
data[1] = 0.0f;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GL_VIEWPORT_BOUNDS_RANGE:
|
||||||
|
g_fake.viewportArrayLimitsQueried = true;
|
||||||
|
data[0] = 0.0f;
|
||||||
|
data[1] = 0.0f;
|
||||||
|
break;
|
||||||
// Two-component range queries.
|
// Two-component range queries.
|
||||||
case GL_ALIASED_LINE_WIDTH_RANGE:
|
case GL_ALIASED_LINE_WIDTH_RANGE:
|
||||||
case GL_SMOOTH_LINE_WIDTH_RANGE:
|
|
||||||
case GL_ALIASED_POINT_SIZE_RANGE:
|
case GL_ALIASED_POINT_SIZE_RANGE:
|
||||||
case GL_VIEWPORT_BOUNDS_RANGE:
|
|
||||||
data[0] = 0.0f;
|
data[0] = 0.0f;
|
||||||
data[1] = 0.0f;
|
data[1] = 0.0f;
|
||||||
break;
|
break;
|
||||||
@@ -642,6 +709,186 @@ TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheS
|
|||||||
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_MAX_CLIP_DISTANCES is the same defect as the per-stage storage blocks above, one pname
|
||||||
|
// over: the query does not exist without GL_EXT_clip_cull_distance, so an unguarded probe left
|
||||||
|
// an optimistic 8 behind on every ARM driver. Advertising eight clip planes a driver cannot host
|
||||||
|
// does not make gl_ClipDistance work - SPIRV-Cross emits it behind an `#extension ... : require`
|
||||||
|
// the ESSL compiler rejects, DirectGLES has nowhere to put the per-distance enables, and the
|
||||||
|
// draw renders nothing while LINK_STATUS says everything is fine.
|
||||||
|
TEST(ClipDistanceCapabilities, NoExtensionMeansNoClipDistancesAndNoQuery) {
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_FALSE(caps.SupportsClipDistance);
|
||||||
|
EXPECT_EQ(caps.MaxClipDistances, 0);
|
||||||
|
EXPECT_FALSE(g_fake.maxClipDistancesQueried)
|
||||||
|
<< "GL_MAX_CLIP_DISTANCES is not ES core; asking for it without the extension only leaks "
|
||||||
|
"a GL_INVALID_ENUM";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half of the same claim, and the one that keeps this from being a blanket zero: a
|
||||||
|
// driver that HAS the extension must have its real limit come through untouched. Adreno does,
|
||||||
|
// and it passes the clip-distance conformance cases on the strength of it.
|
||||||
|
TEST(ClipDistanceCapabilities, TheExtensionIsQueriedAndItsLimitIsReportedVerbatim) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
|
||||||
|
g_fake.maxClipDistances = 6;
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_TRUE(caps.SupportsClipDistance);
|
||||||
|
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
|
||||||
|
EXPECT_EQ(caps.MaxClipDistances, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A driver that advertises the extension and then refuses the query is a driver fault, not a
|
||||||
|
// missing feature - but the answer has to be the honest zero either way, and the error must not
|
||||||
|
// be left for the application's first glGetError to find.
|
||||||
|
TEST(ClipDistanceCapabilities, ARejectedQueryIsDrainedAndReportsZero) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.extensions.emplace_back("GL_EXT_clip_cull_distance");
|
||||||
|
g_fake.clipDistanceQueryRaisesError = true;
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_TRUE(g_fake.maxClipDistancesQueried);
|
||||||
|
EXPECT_EQ(caps.MaxClipDistances, 0);
|
||||||
|
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR) << "the failed query must not leave an error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same defect one more time, for the three GL_OES_viewport_array pnames. Their advertised
|
||||||
|
// values do not come from the driver (GL_Getter answers GL_MAX_VIEWPORTS from the frontend state
|
||||||
|
// width and floors GL_SUBPIXEL_BITS at its own constant), so what this pins is the other half of
|
||||||
|
// the class defect: a pname that does not exist must not be asked for, because the GL_INVALID_ENUM
|
||||||
|
// it raises is then attributed to whatever the application calls next.
|
||||||
|
TEST(ViewportArrayCapabilities, TheLimitsAreOnlyAskedForWhenTheExtensionIsPresent) {
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
MobileGL::MG_External::GLESCapabilities withoutCaps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withoutCaps, funcs));
|
||||||
|
EXPECT_FALSE(withoutCaps.SupportsViewportArray);
|
||||||
|
EXPECT_FALSE(g_fake.viewportArrayLimitsQueried);
|
||||||
|
EXPECT_EQ(withoutCaps.MaxViewports, 16) << "the OpenGL core minimum, not a driver answer";
|
||||||
|
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMin, -32768.0f);
|
||||||
|
EXPECT_FLOAT_EQ(withoutCaps.ViewportBoundsRangeMax, 32767.0f);
|
||||||
|
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.extensions.emplace_back("GL_OES_viewport_array");
|
||||||
|
MobileGL::MG_External::GLESCapabilities withCaps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(withCaps, funcs));
|
||||||
|
EXPECT_TRUE(withCaps.SupportsViewportArray);
|
||||||
|
EXPECT_TRUE(g_fake.viewportArrayLimitsQueried);
|
||||||
|
EXPECT_EQ(withCaps.MaxViewports, g_fake.maxViewports);
|
||||||
|
EXPECT_EQ(withCaps.ViewportSubpixelBits, g_fake.viewportSubpixelBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_LAYER_PROVOKING_VERTEX and GL_VIEWPORT_INDEX_PROVOKING_VERTEX name which vertex of a
|
||||||
|
// primitive supplies gl_Layer and gl_ViewportIndex. MobileGL used to answer a hard-coded
|
||||||
|
// GL_LAST_VERTEX_CONVENTION for both, derived from nothing, and got it wrong on both test devices
|
||||||
|
// in OPPOSITE directions. GL_UNDEFINED_VERTEX is a legal answer (GL 4.6 table 23.65) and it is
|
||||||
|
// the honest one wherever the capability that would give the convention meaning is absent.
|
||||||
|
TEST(ProvokingVertexConventions, AreTakenFromTheDriverOnlyWhereThePnameExists) {
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
// ES 3.1, no viewport array: neither pname exists, so neither is asked for.
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
MobileGL::MG_External::GLESCapabilities es31Caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
||||||
|
EXPECT_FALSE(g_fake.layerProvokingVertexQueried);
|
||||||
|
EXPECT_EQ(es31Caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
EXPECT_EQ(es31Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
|
||||||
|
// ES 3.2 with the viewport array: both exist and both driver answers come through verbatim.
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.glesMinorVersion = 2;
|
||||||
|
g_fake.extensions.emplace_back("GL_OES_viewport_array");
|
||||||
|
MobileGL::MG_External::GLESCapabilities es32Caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
||||||
|
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
|
||||||
|
EXPECT_EQ(es32Caps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||||
|
EXPECT_EQ(es32Caps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_LAST_VERTEX_CONVENTION));
|
||||||
|
|
||||||
|
// ES 3.2 WITHOUT the viewport array - the shape of both test devices. The layer convention is
|
||||||
|
// real and comes from the driver; the viewport-index one describes a selection that never
|
||||||
|
// happens, because only viewport 0 is ever rasterized, and stays undefined.
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.glesMinorVersion = 2;
|
||||||
|
MobileGL::MG_External::GLESCapabilities deviceLikeCaps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(deviceLikeCaps, funcs));
|
||||||
|
EXPECT_EQ(deviceLikeCaps.LayerProvokingVertex, static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||||
|
EXPECT_EQ(deviceLikeCaps.ViewportIndexProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A driver answering something that is not one of the four legal conventions must not have it
|
||||||
|
// forwarded as one: GL_UNDEFINED_VERTEX describes "MobileGL cannot tell you" exactly.
|
||||||
|
TEST(ProvokingVertexConventions, AnIllegalDriverAnswerBecomesUndefined) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.glesMinorVersion = 2;
|
||||||
|
g_fake.layerProvokingVertex = 0x1234;
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_TRUE(g_fake.layerProvokingVertexQueried);
|
||||||
|
EXPECT_EQ(caps.LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The multisample ceilings are ES 3.1 state; a driver that answers zero - or an older context
|
||||||
|
// that answers nothing - must not have that reach GL_Getter, which would then reject the sample
|
||||||
|
// count it just advertised.
|
||||||
|
TEST(MultisampleCapabilities, TheAdvertisedSampleCountsNeverFallBelowOne) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.multisampleCeiling = 0;
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_EQ(caps.MaxColorTextureSamples, 1);
|
||||||
|
EXPECT_EQ(caps.MaxDepthTextureSamples, 1);
|
||||||
|
EXPECT_EQ(caps.MaxFramebufferSamples, 1);
|
||||||
|
EXPECT_EQ(caps.MaxIntegerSamples, 1);
|
||||||
|
EXPECT_EQ(caps.MaxSamples, 1);
|
||||||
|
EXPECT_EQ(caps.MaxSampleMaskWords, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole point of the drain, stated once at the level that matters: capability init is the
|
||||||
|
// first thing that ever touches the driver, so an error it leaves behind surfaces at the
|
||||||
|
// APPLICATION's first glGetError and is blamed on an unrelated call. GL_SMOOTH_LINE_WIDTH_RANGE
|
||||||
|
// is the stand-in because it is desktop-only state that every real GLES driver refuses.
|
||||||
|
TEST(CapabilityProbeHygiene, ARejectedUnconditionalProbeLeavesNoErrorBehind) {
|
||||||
|
ResetFakeDriver();
|
||||||
|
g_fake.maxVertexSsboBlocks = 0;
|
||||||
|
g_fake.smoothLineWidthQueryRaisesError = true;
|
||||||
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
|
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
||||||
|
|
||||||
|
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR)
|
||||||
|
<< "capability init must not hand the application an error it never caused";
|
||||||
|
}
|
||||||
|
|
||||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||||
const auto funcs = MakeFakeGLESFunctions();
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
@@ -267,6 +268,116 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
|
|||||||
ASSERT_EQ(actual, expected);
|
ASSERT_EQ(actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_MIN_MAP_BUFFER_ALIGNMENT is a promise about POINTERS, and MobileGL used to keep only the
|
||||||
|
// query half of it: glGetIntegerv answered 64 while every mapped pointer came out of a plain
|
||||||
|
// std::vector, aligned to alignof(std::max_align_t) - 16 on aarch64. GL 4.2 /
|
||||||
|
// ARB_map_buffer_alignment fix the minimum at 64, so under-reporting is not available and the
|
||||||
|
// implementation has to be brought up to the number instead. Note the two different constraints:
|
||||||
|
// glMapBuffer's pointer must be aligned outright, while glMapBufferRange's must be aligned AFTER
|
||||||
|
// subtracting the offset the caller asked for - i.e. it sits at the offset's own alignment phase.
|
||||||
|
// KHR-GLxx.map_buffer_alignment.functional asserts exactly these two, at offset 63, for 24
|
||||||
|
// storage-flag combinations across 14 targets, and failed identically on both test devices.
|
||||||
|
TEST_F(BufferTest, MappedPointersHonourTheAdvertisedMapBufferAlignment) {
|
||||||
|
GLint advertisedAlignment = 0;
|
||||||
|
MobileGL::MG_Impl::GLImpl::GetIntegerv(GL_MIN_MAP_BUFFER_ALIGNMENT, &advertisedAlignment);
|
||||||
|
ASSERT_EQ(advertisedAlignment, static_cast<GLint>(MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT))
|
||||||
|
<< "the query and the allocator must read the same constant";
|
||||||
|
ASSERT_GE(advertisedAlignment, 64) << "GL 4.2 fixes the minimum at 64";
|
||||||
|
const SizeT alignment = static_cast<SizeT>(advertisedAlignment);
|
||||||
|
|
||||||
|
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||||
|
Vector<Uint> bufferNames;
|
||||||
|
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
|
||||||
|
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
|
||||||
|
slot.Bind(bufObj);
|
||||||
|
|
||||||
|
// The conformance test's own shape: a buffer two alignments long, mapped from the last byte
|
||||||
|
// inside the first alignment - the offset most likely to expose a base-aligned-only fix.
|
||||||
|
const SizeT bufferSize = 2 * alignment;
|
||||||
|
const SizeT offset = alignment - 1;
|
||||||
|
bufObj->Resize(bufferSize);
|
||||||
|
Vector<Uint8> initData(bufferSize);
|
||||||
|
for (SizeT i = 0; i < bufferSize; ++i) initData[i] = static_cast<Uint8>(i);
|
||||||
|
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
|
||||||
|
|
||||||
|
const auto addressOf = [](const void* pointer) { return reinterpret_cast<std::uintptr_t>(pointer); };
|
||||||
|
|
||||||
|
// glMapBuffer, read-only: the shadow base itself is handed out.
|
||||||
|
void* readMapped = bufObj->AcquireMemory(true, true, false);
|
||||||
|
ASSERT_NE(readMapped, nullptr);
|
||||||
|
EXPECT_EQ(addressOf(readMapped) % alignment, 0u) << "glMapBuffer(GL_READ_ONLY) returned an unaligned pointer";
|
||||||
|
bufObj->ReleaseMemory();
|
||||||
|
|
||||||
|
// glMapBuffer, write: the staging store is handed out instead.
|
||||||
|
void* writeMapped = bufObj->AcquireMemory(true, false, true);
|
||||||
|
ASSERT_NE(writeMapped, nullptr);
|
||||||
|
EXPECT_EQ(addressOf(writeMapped) % alignment, 0u) << "glMapBuffer(GL_WRITE_ONLY) returned an unaligned pointer";
|
||||||
|
EXPECT_EQ(bufObj->GetMappedPointer(), writeMapped)
|
||||||
|
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
|
||||||
|
bufObj->ReleaseMemory();
|
||||||
|
|
||||||
|
// glMapBufferRange, read-only: shadow base + offset, so the phase falls out for free.
|
||||||
|
const Range1D mapRange{.start = offset, .end = bufferSize};
|
||||||
|
void* rangeRead = bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Read);
|
||||||
|
ASSERT_NE(rangeRead, nullptr);
|
||||||
|
EXPECT_EQ((addressOf(rangeRead) - offset) % alignment, 0u)
|
||||||
|
<< "glMapBufferRange(READ) returned a pointer whose base is unaligned";
|
||||||
|
bufObj->ReleaseMemory();
|
||||||
|
|
||||||
|
// glMapBufferRange, write: the staging store has to be biased to the same phase, and the
|
||||||
|
// write-back has to follow the bias or the bytes land at the wrong place in the shadow.
|
||||||
|
Uint8* rangeWrite = static_cast<Uint8*>(bufObj->AcquireMemoryRange(mapRange, BufferMappingAccessBit::Write));
|
||||||
|
ASSERT_NE(rangeWrite, nullptr);
|
||||||
|
EXPECT_EQ((addressOf(rangeWrite) - offset) % alignment, 0u)
|
||||||
|
<< "glMapBufferRange(WRITE) returned a pointer whose base is unaligned";
|
||||||
|
EXPECT_EQ(bufObj->GetMappedPointer(), rangeWrite)
|
||||||
|
<< "GL_BUFFER_MAP_POINTER must report the pointer the map returned";
|
||||||
|
// Seeded from the shadow, so the mapped view starts at the offset's byte.
|
||||||
|
EXPECT_EQ(rangeWrite[0], static_cast<Uint8>(offset));
|
||||||
|
rangeWrite[0] = 0xAB;
|
||||||
|
rangeWrite[bufferSize - offset - 1] = 0xCD;
|
||||||
|
bufObj->ReleaseMemory();
|
||||||
|
|
||||||
|
Vector<Uint8> readBack(bufferSize);
|
||||||
|
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
|
||||||
|
EXPECT_EQ(readBack[offset], 0xAB) << "the biased staging write-back landed at the wrong offset";
|
||||||
|
EXPECT_EQ(readBack[bufferSize - 1], 0xCD) << "the biased staging write-back landed at the wrong offset";
|
||||||
|
EXPECT_EQ(readBack[offset - 1], static_cast<Uint8>(offset - 1)) << "the write-back overran the mapped range";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The explicit-flush path reads through the same bias, one flush offset further in: a flush of
|
||||||
|
// [offset + 4, offset + 8) must copy the bytes the application wrote at rangeWrite[4..8), not the
|
||||||
|
// ones sitting four bytes into the raw allocation.
|
||||||
|
TEST_F(BufferTest, ExplicitFlushOfARangeMapFollowsTheAlignmentBias) {
|
||||||
|
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
|
||||||
|
Vector<Uint> bufferNames;
|
||||||
|
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
|
||||||
|
auto bufObj = MobileGL::MG_State::pGLContext->CreateBufferObject(bufferNames[0]);
|
||||||
|
slot.Bind(bufObj);
|
||||||
|
|
||||||
|
const SizeT alignment = MobileGL::MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT;
|
||||||
|
const SizeT bufferSize = 2 * alignment;
|
||||||
|
const SizeT offset = alignment - 1;
|
||||||
|
bufObj->Resize(bufferSize);
|
||||||
|
Vector<Uint8> initData(bufferSize, 0);
|
||||||
|
bufObj->UploadData(DataPtr{.data = initData.data(), .size = bufferSize}, 0);
|
||||||
|
|
||||||
|
const Range1D mapRange{.start = offset, .end = bufferSize};
|
||||||
|
Uint8* mapped = static_cast<Uint8*>(bufObj->AcquireMemoryRange(
|
||||||
|
mapRange, BufferMappingAccessBit::Write | BufferMappingAccessBit::FlushExplicit));
|
||||||
|
ASSERT_NE(mapped, nullptr);
|
||||||
|
mapped[4] = 0x5A;
|
||||||
|
mapped[5] = 0x5B;
|
||||||
|
bufObj->FlushMemoryRange(4, 2);
|
||||||
|
bufObj->ReleaseMemory();
|
||||||
|
|
||||||
|
Vector<Uint8> readBack(bufferSize);
|
||||||
|
bufObj->DownloadSubData(readBack.data(), 0, bufferSize);
|
||||||
|
EXPECT_EQ(readBack[offset + 4], 0x5A);
|
||||||
|
EXPECT_EQ(readBack[offset + 5], 0x5B);
|
||||||
|
EXPECT_EQ(readBack[offset + 3], 0x00) << "the explicit flush copied bytes outside the flushed range";
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(BufferTest, CopyBufferSubData) {
|
TEST_F(BufferTest, CopyBufferSubData) {
|
||||||
auto& srcSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyRead);
|
auto& srcSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyRead);
|
||||||
auto& dstSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyWrite);
|
auto& dstSlot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::CopyWrite);
|
||||||
|
|||||||
@@ -716,6 +716,177 @@ void main() {
|
|||||||
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
|
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two counters that share a binding AND an offset must fail to link. glslang's own check
|
||||||
|
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
|
||||||
|
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
|
||||||
|
// used to link cleanly and then increment the same four bytes.
|
||||||
|
TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) {
|
||||||
|
const char* fs = R"(#version 430
|
||||||
|
out vec4 color;
|
||||||
|
layout (binding = 0, offset = 0) uniform atomic_uint a;
|
||||||
|
layout (binding = 0, offset = 0) uniform atomic_uint b;
|
||||||
|
void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); }
|
||||||
|
)";
|
||||||
|
const GLuint p = MakeProgram(kSimpleVs, fs);
|
||||||
|
LinkProgram(p);
|
||||||
|
GLint status = -1;
|
||||||
|
GetProgramiv(p, GL_LINK_STATUS, &status);
|
||||||
|
EXPECT_EQ(status, GL_FALSE);
|
||||||
|
char log[4096] = "";
|
||||||
|
GetProgramInfoLog(p, sizeof(log), nullptr, log);
|
||||||
|
EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log;
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
// Distinct offsets at one binding, and the same offset at two different bindings, are
|
||||||
|
// both legal and must still link - a check keyed any wider would reject them.
|
||||||
|
const char* legalFs = R"(#version 430
|
||||||
|
out vec4 color;
|
||||||
|
layout (binding = 0, offset = 0) uniform atomic_uint a;
|
||||||
|
layout (binding = 0, offset = 4) uniform atomic_uint b;
|
||||||
|
layout (binding = 1, offset = 0) uniform atomic_uint c;
|
||||||
|
void main() {
|
||||||
|
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint legal = MakeProgram(kSimpleVs, legalFs);
|
||||||
|
LinkProgram(legal);
|
||||||
|
ExpectLinked(legal);
|
||||||
|
ClearErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
|
||||||
|
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
|
||||||
|
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
|
||||||
|
// them - so every deliberately-oversized program in
|
||||||
|
// KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing.
|
||||||
|
//
|
||||||
|
// Sized off the ADVERTISED limits rather than a constant, because the numbers come from the
|
||||||
|
// active backend and the whole point of the check is that the two agree.
|
||||||
|
TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) {
|
||||||
|
GLint maxFragmentImages = 0;
|
||||||
|
GLint maxCombinedImages = 0;
|
||||||
|
GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
|
||||||
|
GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages);
|
||||||
|
ClearErrors();
|
||||||
|
ASSERT_GT(maxFragmentImages, 0);
|
||||||
|
|
||||||
|
// The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken
|
||||||
|
// for the link failure under test.
|
||||||
|
const auto linkWithFragmentImages = [](GLint count) {
|
||||||
|
const std::string n = std::to_string(count);
|
||||||
|
const std::string source = std::string(R"(#version 430
|
||||||
|
out vec4 color;
|
||||||
|
layout(r32i) uniform iimage2D u_image[)") + n + R"(];
|
||||||
|
void main() {
|
||||||
|
int value = 1;
|
||||||
|
for (int i = 0; i < )" + n + R"(; ++i) {
|
||||||
|
value = imageAtomicAdd(u_image[i], ivec2(0), value);
|
||||||
|
}
|
||||||
|
color = vec4(float(value));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const char* sourcePtr = source.c_str();
|
||||||
|
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
ShaderSource(fs, 1, &sourcePtr, nullptr);
|
||||||
|
CompileShader(fs);
|
||||||
|
GLint compiled = 0;
|
||||||
|
GetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||||
|
EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile";
|
||||||
|
|
||||||
|
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||||
|
ShaderSource(vs, 1, &kSimpleVs, nullptr);
|
||||||
|
CompileShader(vs);
|
||||||
|
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, vs);
|
||||||
|
AttachShader(program, fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
return program;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLuint over = linkWithFragmentImages(maxFragmentImages + 1);
|
||||||
|
GLint status = -1;
|
||||||
|
GetProgramiv(over, GL_LINK_STATUS, &status);
|
||||||
|
EXPECT_EQ(status, GL_FALSE);
|
||||||
|
char log[4096] = "";
|
||||||
|
GetProgramInfoLog(over, sizeof(log), nullptr, log);
|
||||||
|
EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos)
|
||||||
|
<< "info log was: " << log;
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
// Exactly AT the limit is legal and must still link: the comparison is strictly
|
||||||
|
// greater-than, and the conformance suite's combined-stage subcase builds a program that
|
||||||
|
// fills every stage to its own limit and expects it to link whenever the combined limit
|
||||||
|
// can hold them.
|
||||||
|
if (maxFragmentImages <= maxCombinedImages) {
|
||||||
|
const GLuint atLimit = linkWithFragmentImages(maxFragmentImages);
|
||||||
|
ExpectLinked(atLimit);
|
||||||
|
ClearErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are
|
||||||
|
// the pre-4.3 spelling of the interface above, and the spec requires the two to agree.
|
||||||
|
// Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the
|
||||||
|
// relaxed parse folds every atomic_uint into a storage block before reflection runs - and
|
||||||
|
// the second was a stub that wrote nothing and raised nothing.
|
||||||
|
TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) {
|
||||||
|
const char* fs = R"(#version 430
|
||||||
|
out vec4 color;
|
||||||
|
layout (binding = 1, offset = 0) uniform atomic_uint a;
|
||||||
|
layout (binding = 2, offset = 0) uniform atomic_uint b;
|
||||||
|
layout (binding = 2, offset = 4) uniform atomic_uint c;
|
||||||
|
void main() {
|
||||||
|
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint p = MakeProgram(kSimpleVs, fs);
|
||||||
|
LinkProgram(p);
|
||||||
|
ExpectLinked(p);
|
||||||
|
ClearErrors();
|
||||||
|
|
||||||
|
GLint bufferCount = -12345;
|
||||||
|
GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount);
|
||||||
|
EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
|
||||||
|
ASSERT_EQ(bufferCount, 2);
|
||||||
|
|
||||||
|
const auto activeBufferiv = [p](GLuint index, GLenum pname) {
|
||||||
|
GLint value = -12345;
|
||||||
|
GetActiveAtomicCounterBufferiv(p, index, pname, &value);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
for (GLuint index = 0; index < static_cast<GLuint>(bufferCount); ++index) {
|
||||||
|
const std::vector<GLint> viaInterface =
|
||||||
|
Props(p, GL_ATOMIC_COUNTER_BUFFER, index,
|
||||||
|
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES,
|
||||||
|
GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER});
|
||||||
|
ASSERT_EQ(viaInterface.size(), 5u);
|
||||||
|
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_BINDING), viaInterface[0]);
|
||||||
|
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE), viaInterface[1]);
|
||||||
|
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS), viaInterface[2]);
|
||||||
|
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER), viaInterface[3]);
|
||||||
|
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER), viaInterface[4]);
|
||||||
|
|
||||||
|
// The counter indices are the GL_UNIFORM indices, in the same order.
|
||||||
|
const std::vector<GLint> expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES});
|
||||||
|
ASSERT_FALSE(expectedIndices.empty());
|
||||||
|
std::vector<GLint> indices(expectedIndices.size(), -12345);
|
||||||
|
GetActiveAtomicCounterBufferiv(p, index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
|
||||||
|
indices.data());
|
||||||
|
EXPECT_EQ(indices, expectedIndices);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(TakeError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLint sink = -12345;
|
||||||
|
GetActiveAtomicCounterBufferiv(p, static_cast<GLuint>(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink);
|
||||||
|
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
|
||||||
|
EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output";
|
||||||
|
// The interface-query spelling of the same property is NOT accepted here.
|
||||||
|
GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink);
|
||||||
|
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
|
||||||
|
EXPECT_EQ(sink, -12345);
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------- transform-feedback ------
|
// --------------------------------------------------------- transform-feedback ------
|
||||||
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
|
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
|
||||||
const char* vs = R"(#version 430
|
const char* vs = R"(#version 430
|
||||||
|
|||||||
@@ -3010,21 +3010,38 @@ TEST_F(ProgramTest, TwoShaderObjectsWithIdenticalSourceLinkIndependently) {
|
|||||||
ASSERT_NE(objectA, nullptr);
|
ASSERT_NE(objectA, nullptr);
|
||||||
ASSERT_NE(objectB, nullptr);
|
ASSERT_NE(objectB, nullptr);
|
||||||
EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource());
|
EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource());
|
||||||
// P0b's layer 2 shares the PREPROCESS and never the parse: glslang's TShader is
|
// WHAT THIS CASE IS ACTUALLY ABOUT: two GL shader names holding the same text must never
|
||||||
// consume-once, so a memo hit still has to parse for itself.
|
// end up feeding one TShader to two links, because mapIO mutates the aliased intermediate
|
||||||
|
// and the second link would get a corrupted one. There are now three mechanisms that keep
|
||||||
|
// that true, and which one is in play depends on the mode - so the assertion below is on
|
||||||
|
// the PARSES NOT BEING SHARED, never on where each object's parse came from:
|
||||||
//
|
//
|
||||||
// P1 stage 6 shares something stronger when it is active - the whole compile JOB, and
|
// * P0b layer 2 shares the PREPROCESS and never the parse, so each object parses for
|
||||||
// therefore the single parse that job produced - and that sharing is made safe by
|
// itself. This was the only mechanism when the case was written.
|
||||||
// ShaderCompileTask::ClaimParsedShader's CAS instead, exactly as it already was for one
|
// * P1 stage 6, when async is active, shares the whole compile JOB and therefore its
|
||||||
// shader object attached to two programs. ShaderCompileAdoptionTest is where that is
|
// single parse - made safe by ClaimParsedShader's CAS, exactly as it already was for
|
||||||
// pinned down (it links both objects and compares the generated SPIR-V). So the
|
// one shader object attached to two programs. ShaderCompileAdoptionTest pins that
|
||||||
// one-parse-per-object assertion belongs to the non-adopting path; the two independent
|
// down by linking both objects and comparing the generated SPIR-V.
|
||||||
// LINKS below are what both modes have to agree on, and they are the point of this case.
|
// * The translation memo's compile half (L1c) recognises the second object's source and
|
||||||
|
// publishes its verdict WITHOUT parsing, so that object legitimately holds no TShader
|
||||||
|
// at all until a link asks ClaimParsedShader for one. Asserting a non-null parse here
|
||||||
|
// would be asserting that the parse had NOT been skipped - i.e. testing the absence
|
||||||
|
// of the optimisation rather than the invariant.
|
||||||
|
//
|
||||||
|
// So the pointer assertion applies only where the two objects are genuinely INDEPENDENT,
|
||||||
|
// i.e. where job adoption is not in play. What every mode has to agree on is the two
|
||||||
|
// independent LINKS below, and they are the real point of this case.
|
||||||
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
||||||
EXPECT_NE(objectA->GetCompiledShader(), objectB->GetCompiledShader());
|
const auto& shaderA = objectA->GetCompiledShader();
|
||||||
|
const auto& shaderB = objectB->GetCompiledShader();
|
||||||
|
// Either may legitimately hold NO parse: that is an L1c hit, where the AST is made on
|
||||||
|
// demand at link instead. So this asserts they are not the SAME non-null parse, and
|
||||||
|
// deliberately not that both have one - the latter would be asserting that the
|
||||||
|
// optimisation had not happened.
|
||||||
|
if (shaderA != nullptr && shaderB != nullptr) {
|
||||||
|
EXPECT_NE(shaderA, shaderB) << "two independent shader objects share one consume-once parse";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EXPECT_NE(objectA->GetCompiledShader(), nullptr);
|
|
||||||
EXPECT_NE(objectB->GetCompiledShader(), nullptr);
|
|
||||||
|
|
||||||
GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE);
|
GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE);
|
||||||
GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE);
|
GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE);
|
||||||
@@ -3239,3 +3256,76 @@ TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasse
|
|||||||
EXPECT_NE(program, 0u);
|
EXPECT_NE(program, 0u);
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARB_explicit_uniform_location / GL 4.6 core 7.6.1: a `layout(location = N)` uniform reserves N
|
||||||
|
// EVEN WHEN IT IS INACTIVE. Dead default-block uniforms are correctly filtered off the GL surface
|
||||||
|
// (glGetUniformLocation must answer -1 for them), but the implicit allocator used to walk straight
|
||||||
|
// over the location they claimed and hand it to a uniform that never asked for it
|
||||||
|
// (KHR-GL43.explicit_uniform_location.uniform-loc-mix-with-implicit3).
|
||||||
|
TEST_F(ProgramTest, InactiveExplicitUniformLocationIsStillReserved) {
|
||||||
|
const char* vsSource = R"(#version 430 core
|
||||||
|
layout(location = 2) uniform vec4 uDeadAtTwo;
|
||||||
|
uniform vec4 uA;
|
||||||
|
uniform vec4 uB;
|
||||||
|
uniform vec4 uC;
|
||||||
|
uniform vec4 uD;
|
||||||
|
void main() { gl_Position = uA + uB + uC + uD; }
|
||||||
|
)";
|
||||||
|
const char* fsSource = R"(#version 430 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||||
|
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
|
||||||
|
// Reserving a location must not resurrect the uniform: it is still inactive to GL.
|
||||||
|
EXPECT_EQ(GetUniformLocation(program, "uDeadAtTwo"), -1);
|
||||||
|
|
||||||
|
for (const char* name : {"uA", "uB", "uC", "uD"}) {
|
||||||
|
const GLint location = GetUniformLocation(program, name);
|
||||||
|
EXPECT_GE(location, 0) << name << " lost its implicit location";
|
||||||
|
EXPECT_NE(location, 2) << name << " was handed the location uDeadAtTwo reserved";
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL_MAX_UNIFORM_LOCATIONS boundary, from both sides. MAX_UNIFORM_LOCATIONS - 1 is the LAST
|
||||||
|
// LEGAL location: it has to link and read back verbatim
|
||||||
|
// (KHR-GL43.explicit_uniform_location.uniform-loc-max), which is only true while the advertised
|
||||||
|
// value and what the link accepts are the SAME number - the getter used to advertise one more
|
||||||
|
// location than any shader could name.
|
||||||
|
//
|
||||||
|
// The over-the-ceiling half is asserted through an ARRAY, because that is the only spelling the
|
||||||
|
// link gets to judge: a bare `layout(location = MAX)` is already a compile error inside glslang
|
||||||
|
// ("location is too large"), while an array's base compiles fine and only its last element passes
|
||||||
|
// the ceiling (...uniform-loc-negative-link-max-num-of-locations).
|
||||||
|
TEST_F(ProgramTest, ExplicitUniformLocationsHonourMaxUniformLocations) {
|
||||||
|
GLint maxLocations = 0;
|
||||||
|
GetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &maxLocations);
|
||||||
|
ASSERT_GE(maxLocations, 1024) << "GL 4.3 requires at least 1024 uniform locations";
|
||||||
|
|
||||||
|
const char* fsSource = R"(#version 430 core
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() { fragColor = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
const GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String source = String("#version 430 core\nlayout(location = ") +
|
||||||
|
std::to_string(maxLocations - 1) +
|
||||||
|
") uniform vec4 uAtLimit;\nvoid main() { gl_Position = uAtLimit; }\n";
|
||||||
|
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
|
||||||
|
const GLuint program = LinkVsFs(vs, fs, GL_TRUE);
|
||||||
|
EXPECT_EQ(GetUniformLocation(program, "uAtLimit"), maxLocations - 1)
|
||||||
|
<< "the last location in the pool is legal and must come back verbatim";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const String source = String("#version 430 core\nlayout(location = ") +
|
||||||
|
std::to_string(maxLocations - 4) +
|
||||||
|
") uniform vec4 uSpill[8];\nvoid main() { gl_Position = uSpill[0]; }\n";
|
||||||
|
const GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, source.c_str());
|
||||||
|
(void)LinkVsFs(vs, fs, GL_FALSE);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3840,3 +3840,228 @@ TEST_F(ProgramUtilTest, EsslCoreImageFormatSetIsTheThirteenTheSpecLists) {
|
|||||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0x8051 /*GL_RGB8*/));
|
||||||
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
EXPECT_FALSE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(0 /*GL_NONE*/));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6. glslang assigns a block's member
|
||||||
|
// offsets at DECLARATION time, where a member array that is still unsized contributes zero bytes -
|
||||||
|
// so `vec4 position01[]; vec4 position2;` put both members at offset 0 and the shader read
|
||||||
|
// position01[0] where it asked for position2. The preprocessor sizes the non-final member from the
|
||||||
|
// largest constant index the source uses, which is what the language says it means.
|
||||||
|
TEST_F(ProgramUtilTest, ANonFinalUnsizedBufferBlockMemberIsSizedFromItsLargestConstantIndex) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
String source = R"(#version 430 core
|
||||||
|
layout(packed) coherent buffer Buffer {
|
||||||
|
vec4 position01[];
|
||||||
|
vec4 position2;
|
||||||
|
} g_buffer;
|
||||||
|
void main() {
|
||||||
|
if (gl_VertexID == 0) gl_Position = g_buffer.position01[0];
|
||||||
|
else if (gl_VertexID == 1) gl_Position = g_buffer.position01[1];
|
||||||
|
else if (gl_VertexID == 2) gl_Position = g_buffer.position2;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||||
|
EXPECT_NE(source.find("vec4 position01[2];"), String::npos) << source;
|
||||||
|
EXPECT_EQ(source.find("position01[];"), String::npos) << source;
|
||||||
|
|
||||||
|
// The LAST member of a storage block is a run-time sized array, which is legal and already
|
||||||
|
// laid out correctly - sizing it would be a wire-format change, not a repair.
|
||||||
|
String lastMember = R"(#version 430 core
|
||||||
|
buffer Buffer {
|
||||||
|
vec4 head;
|
||||||
|
vec4 tail[];
|
||||||
|
} g_buffer;
|
||||||
|
void main() {
|
||||||
|
gl_Position = g_buffer.tail[0] + g_buffer.tail[3];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Vertex, lastMember);
|
||||||
|
EXPECT_NE(lastMember.find("vec4 tail[];"), String::npos) << lastMember;
|
||||||
|
|
||||||
|
// A member the shader subscripts with anything but a literal cannot be sized from the source,
|
||||||
|
// so it is left exactly as it was.
|
||||||
|
String dynamicIndex = R"(#version 430 core
|
||||||
|
buffer Buffer {
|
||||||
|
vec4 head[];
|
||||||
|
vec4 tail;
|
||||||
|
} g_buffer;
|
||||||
|
uniform int g_index;
|
||||||
|
void main() {
|
||||||
|
gl_Position = g_buffer.head[g_index] + g_buffer.tail;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Vertex, dynamicIndex);
|
||||||
|
EXPECT_NE(dynamicIndex.find("vec4 head[];"), String::npos) << dynamicIndex;
|
||||||
|
|
||||||
|
// `buffer` is also a member memory qualifier; a declaration that uses it must not be mistaken
|
||||||
|
// for a block header.
|
||||||
|
String memberQualifier = R"(#version 430 core
|
||||||
|
coherent buffer Buffer {
|
||||||
|
buffer vec4 position0;
|
||||||
|
vec4 position1[];
|
||||||
|
vec4 position2;
|
||||||
|
} g_buffer;
|
||||||
|
void main() {
|
||||||
|
gl_Position = g_buffer.position0 + g_buffer.position1[2] + g_buffer.position2;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
PreprocessShaderSource(ShaderStage::Vertex, memberQualifier);
|
||||||
|
EXPECT_NE(memberQualifier.find("vec4 position1[3];"), String::npos) << memberQualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.shader_storage_buffer_object.negative-glsl-compileTime: a storage block declared at
|
||||||
|
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS must fail to compile, and so must an arrayed one whose
|
||||||
|
// LAST element passes the ceiling. The relaxed Vulkan-rules parse enforces neither.
|
||||||
|
TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
constexpr Int kMaxBindings = 36;
|
||||||
|
const auto violation = [](const String& body) {
|
||||||
|
return FindShaderStorageBindingViolation("#version 430 core\n" + body + "void main() {}\n", kMaxBindings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The boundary itself: max - 1 is the last legal point, max is one past it.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 35) buffer Buffer { int x; };\n").has_value());
|
||||||
|
EXPECT_TRUE(violation("layout(binding = 36) buffer Buffer { int x; };\n").has_value());
|
||||||
|
|
||||||
|
// An instance array takes CONSECUTIVE points, so what has to fit is base + count - 1.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 32) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||||
|
EXPECT_TRUE(violation("layout(binding = 34) buffer Buffer { int x; } g_array[4];\n").has_value());
|
||||||
|
|
||||||
|
// Qualifiers and a second layout list may sit between the binding and the keyword.
|
||||||
|
EXPECT_TRUE(violation("layout(std430) layout(binding = 36) coherent restrict buffer B { int x; };\n")
|
||||||
|
.has_value());
|
||||||
|
|
||||||
|
// Things the scanner must NOT judge: a uniform block (a different ceiling), a storage block
|
||||||
|
// with no explicit binding, the bare default-qualifier form, and an instance array whose size
|
||||||
|
// is not a literal.
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 40) uniform Block { int x; };\n"
|
||||||
|
"layout(binding = 0) buffer Buffer { int y; };\n")
|
||||||
|
.has_value());
|
||||||
|
EXPECT_FALSE(violation("buffer Buffer { int x; };\nconst int binding = 40;\n").has_value());
|
||||||
|
EXPECT_FALSE(violation("layout(binding = 1) buffer;\nbuffer Buffer { int x; };\n").has_value());
|
||||||
|
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(binding = 34) buffer B { int x; } g[kCount];\n")
|
||||||
|
.has_value());
|
||||||
|
|
||||||
|
// A backend that advertises no binding points has no ceiling to enforce.
|
||||||
|
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
|
||||||
|
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
|
||||||
|
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
|
||||||
|
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
|
||||||
|
// The identical defect sat on every array dimension and on layout(binding = N).
|
||||||
|
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 0xA) uniform vec4 hexLower;
|
||||||
|
layout(location = 0X1f) uniform vec4 hexUpper;
|
||||||
|
layout(location = 010) uniform vec4 octal;
|
||||||
|
layout(location = 3u) uniform vec4 unsignedSuffix;
|
||||||
|
layout(location = 0x2) uniform float hexArray[0x3];
|
||||||
|
layout(location = 1.0) uniform vec4 notAnInteger;
|
||||||
|
layout(location = 7f) uniform vec4 unknownSuffix;
|
||||||
|
void main() {}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||||
|
ASSERT_EQ(locations.count("hexLower"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexLower"), 10);
|
||||||
|
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexUpper"), 31);
|
||||||
|
ASSERT_EQ(locations.count("octal"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
||||||
|
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
||||||
|
ASSERT_EQ(locations.count("hexArray"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexArray"), 2);
|
||||||
|
|
||||||
|
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
|
||||||
|
EXPECT_EQ(locations.count("notAnInteger"), 0u);
|
||||||
|
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
|
||||||
|
// in the same statement starts at the wrong location.
|
||||||
|
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
|
||||||
|
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
|
||||||
|
ASSERT_EQ(locations.count("first"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("first"), 50);
|
||||||
|
ASSERT_EQ(locations.count("second"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
||||||
|
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
|
||||||
|
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
|
||||||
|
// keys both records missed the map entirely and were first-fitted from location 0.
|
||||||
|
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 2) uniform float two_d[2][3];
|
||||||
|
layout(location = 20) uniform float three_d[2][2][4];
|
||||||
|
layout(location = 40) uniform float one_d[3];
|
||||||
|
void main() {}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
||||||
|
|
||||||
|
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
||||||
|
ASSERT_EQ(locations.count("two_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d"), 2);
|
||||||
|
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
||||||
|
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
||||||
|
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
||||||
|
|
||||||
|
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
||||||
|
ASSERT_EQ(locations.count("three_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d"), 20);
|
||||||
|
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
||||||
|
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
||||||
|
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
||||||
|
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
||||||
|
|
||||||
|
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
||||||
|
ASSERT_EQ(locations.count("one_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("one_d"), 40);
|
||||||
|
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
||||||
|
|
||||||
|
// The declarator after an array-of-arrays still advances by the WHOLE element count.
|
||||||
|
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
|
||||||
|
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
|
||||||
|
ASSERT_EQ(pair.count("b"), 1u);
|
||||||
|
EXPECT_EQ(pair.at("b"), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
|
||||||
|
// as the location one, and losing it costs the sampler its initial texture unit.
|
||||||
|
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(binding = 0x2) uniform sampler2D hexUnit;
|
||||||
|
layout(binding = 012) uniform sampler2D octalUnit;
|
||||||
|
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
||||||
|
void main() {}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
|
||||||
|
ASSERT_EQ(bindings.count("hexUnit"), 1u);
|
||||||
|
EXPECT_EQ(bindings.at("hexUnit"), 2u);
|
||||||
|
ASSERT_EQ(bindings.count("octalUnit"), 1u);
|
||||||
|
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
||||||
|
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
|
||||||
|
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,6 +140,29 @@ namespace {
|
|||||||
|
|
||||||
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
|
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
|
||||||
|
|
||||||
|
// Stub backend occlusion queries. The host has no ES context, and BeginQuery refuses the
|
||||||
|
// occlusion targets outright when the backend advertises no hook - so a conditional-render
|
||||||
|
// test cannot get a legal predicate object without these. g_stubResultNs is the sample count
|
||||||
|
// the "driver" reports, which is the whole input to the predicate.
|
||||||
|
MG_Backend::BackendQueryHandle StubBeginOcclusionQuery() {
|
||||||
|
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x54));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StubEndOcclusionQuery(MG_Backend::BackendQueryHandle) {}
|
||||||
|
|
||||||
|
void InstallStubBackendOcclusionQueries() {
|
||||||
|
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||||
|
backendGL.BeginOcclusionQuery = StubBeginOcclusionQuery;
|
||||||
|
backendGL.EndOcclusionQuery = StubEndOcclusionQuery;
|
||||||
|
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
|
||||||
|
backendGL.GetQueryResult64 = StubGetQueryResult64;
|
||||||
|
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
|
||||||
|
g_stubDeleteCount = 0;
|
||||||
|
g_stubResultAvailable = true;
|
||||||
|
g_stubResultObtainable = true;
|
||||||
|
g_stubResultNs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void InstallStubBackendXfbQueries() {
|
void InstallStubBackendXfbQueries() {
|
||||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
||||||
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
|
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
|
||||||
@@ -677,6 +700,108 @@ TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference)
|
|||||||
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
|
// unified truthy rule (set, non-empty, not "0", case-insensitive not "false").
|
||||||
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
|
// Running the binary under MOBILEGL_DISABLE_TIMERQUERY=1 therefore exercises
|
||||||
// the real end-to-end path rather than the struct field alone.
|
// the real end-to-end path rather than the struct field alone.
|
||||||
|
// KHR-GL43.compute_shader.conditional-dispatching and the conditional_render family.
|
||||||
|
// glBeginConditionalRender/glEndConditionalRender were bare stubs: every command inside a
|
||||||
|
// conditional block executed whatever the query said, so the block that should have been
|
||||||
|
// discarded ran and doubled the atomic counter the case reads back.
|
||||||
|
TEST_F(QueryTest, ConditionalRenderResolvesItsPredicateFromTheOcclusionQuery) {
|
||||||
|
ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendOcclusionQueries();
|
||||||
|
|
||||||
|
GLuint ids[2] = {0, 0};
|
||||||
|
MG_Impl::GLImpl::GenQueries(2, ids);
|
||||||
|
ASSERT_NE(ids[0], 0u);
|
||||||
|
ASSERT_NE(ids[1], 0u);
|
||||||
|
|
||||||
|
// One span that saw samples and one that saw none, which is exactly the pair the
|
||||||
|
// conformance case builds out of a passing and a failing depth test.
|
||||||
|
g_stubResultNs = 1;
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
GLuint passedResult = 0xFFFFFFFFu;
|
||||||
|
MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &passedResult);
|
||||||
|
ASSERT_EQ(passedResult, 1u);
|
||||||
|
|
||||||
|
g_stubResultNs = 0;
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[1]);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// A block on the query that passed executes.
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||||
|
|
||||||
|
// A block on the query that did not passes nothing through.
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
|
||||||
|
// ...and the _INVERTED modes swap both verdicts.
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT_INVERTED);
|
||||||
|
EXPECT_TRUE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_BY_REGION_NO_WAIT_INVERTED);
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->ConditionalRenderDiscardsCommands());
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QueryTest, ConditionalRenderRejectsTheErrorsTheSpecNames) {
|
||||||
|
ScopedBackendFunctionsOverride backendGuard;
|
||||||
|
InstallStubBackendOcclusionQueries();
|
||||||
|
|
||||||
|
GLuint ids[2] = {0, 0};
|
||||||
|
MG_Impl::GLImpl::GenQueries(2, ids);
|
||||||
|
g_stubResultNs = 1;
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_ANY_SAMPLES_PASSED, ids[0]);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_ANY_SAMPLES_PASSED);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// GL 4.6 core 10.9, one rule at a time.
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_TIME_ELAPSED);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_ENUM);
|
||||||
|
EXPECT_FALSE(MG_State::pGLContext->IsConditionalRenderActive());
|
||||||
|
|
||||||
|
// A generated NAME is not yet a query object.
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[1], GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(0, GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// A query that is not an occlusion query cannot drive one.
|
||||||
|
GLuint timerId = 0;
|
||||||
|
MG_Impl::GLImpl::GenQueries(1, &timerId);
|
||||||
|
MG_Impl::GLImpl::BeginQuery(GL_TIME_ELAPSED, timerId);
|
||||||
|
MG_Impl::GLImpl::EndQuery(GL_TIME_ELAPSED);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(timerId, GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||||
|
|
||||||
|
// End without a block, and a nested Begin.
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
MG_Impl::GLImpl::BeginConditionalRender(ids[0], GL_QUERY_WAIT);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
|
||||||
|
// The rejected nested Begin must not have disturbed the open block.
|
||||||
|
EXPECT_EQ(MG_State::pGLContext->GetConditionalRenderQuery(), ids[0]);
|
||||||
|
MG_Impl::GLImpl::EndConditionalRender();
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
||||||
|
MG_Impl::GLImpl::DeleteQueries(1, &timerId);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
TEST_F(QueryTest, DisableTimerQueryFeatureMatchesEnvironment) {
|
||||||
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
|
const char* raw = std::getenv("MOBILEGL_DISABLE_TIMERQUERY");
|
||||||
Bool expected = false;
|
Bool expected = false;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include <MG_Backend/DirectGLES/Managers.h>
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||||
@@ -642,6 +643,55 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
|
|||||||
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxClipDistances is a LIMIT every Vulkan device reports; declaring ClipDistance in a module
|
||||||
|
// needs the shaderClipDistance FEATURE, which is separate and which VulkanRenderer enables only
|
||||||
|
// where the physical device has it. Forwarding the limit without the feature advertises eight
|
||||||
|
// clip planes no shader may use - the same shape as the image-uniform limits above, and the same
|
||||||
|
// shape as the GL_EXT_clip_cull_distance lie on DirectGLES. Not a blanket zero: a device WITH the
|
||||||
|
// feature keeps its real number.
|
||||||
|
TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
|
||||||
|
MG_External::VulkanCapabilities caps;
|
||||||
|
caps.MaxClipDistances = 8;
|
||||||
|
|
||||||
|
caps.SupportsShaderClipDistance = false;
|
||||||
|
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0);
|
||||||
|
|
||||||
|
caps.SupportsShaderClipDistance = true;
|
||||||
|
backend.ApplyVulkanCapabilitiesForTesting(caps);
|
||||||
|
EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded
|
||||||
|
// GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test
|
||||||
|
// devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan
|
||||||
|
// reports GL_UNDEFINED_VERTEX, which GL 4.6 table 23.65 permits and which is what the backend
|
||||||
|
// honestly implements - the provoking mode is chosen per pipeline out of VK_EXT_provoking_vertex,
|
||||||
|
// provokingVertexModePerPipeline and the topology.
|
||||||
|
TEST(ProvokingVertexConventions, EachBackendReportsWhatItActuallyPins) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
|
||||||
|
MG_External::GLESCapabilities glesCaps;
|
||||||
|
glesCaps.LayerProvokingVertex = GL_FIRST_VERTEX_CONVENTION;
|
||||||
|
glesCaps.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
|
||||||
|
EXPECT_EQ(glesBackend.GetDynamicParameters().LayerProvokingVertex,
|
||||||
|
static_cast<GLenum>(GL_FIRST_VERTEX_CONVENTION));
|
||||||
|
EXPECT_EQ(glesBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
|
||||||
|
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
|
||||||
|
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
|
||||||
|
MG_External::VulkanCapabilities vkCaps;
|
||||||
|
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
|
||||||
|
EXPECT_EQ(vkBackend.GetDynamicParameters().LayerProvokingVertex, static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
EXPECT_EQ(vkBackend.GetDynamicParameters().ViewportIndexProvokingVertex,
|
||||||
|
static_cast<GLenum>(GL_UNDEFINED_VERTEX));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
|
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -929,6 +979,161 @@ void main() {
|
|||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.shader_atomic_counters.basic-glsl-built-in, .basic-buffer-bind and .basic-api-get.
|
||||||
|
// The atomic-counter limits used to live in two unreconciled tables - glslang compiled every
|
||||||
|
// shader against ONE binding while glGetIntegerv advertised thirty-six - and three of the enums
|
||||||
|
// had no case in the getter at all, so the query raised INVALID_ENUM and left the caller reading
|
||||||
|
// whatever was in its own stack slot.
|
||||||
|
TEST(GetterSanity, AtomicCounterQueriesMatchShaderCompilerLimits) {
|
||||||
|
using namespace MobileGL;
|
||||||
|
namespace Transpiler = MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
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{});
|
||||||
|
|
||||||
|
GLint reported = -1;
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &reported);
|
||||||
|
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, &reported);
|
||||||
|
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE));
|
||||||
|
for (const GLenum pname : {GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS, GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS,
|
||||||
|
GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS}) {
|
||||||
|
reported = -1;
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(pname, &reported);
|
||||||
|
EXPECT_EQ(reported, static_cast<GLint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE))
|
||||||
|
<< "pname " << pname;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// glBindBufferBase sets the GENERIC binding point too (GL 4.6 6.1.1), and this is the one
|
||||||
|
// indexed-buffer family whose non-indexed query had no case.
|
||||||
|
reported = -1;
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
|
||||||
|
EXPECT_EQ(reported, 0);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
MG_Impl::GLImpl::GenBuffers(1, &buffer);
|
||||||
|
MG_Impl::GLImpl::BindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
||||||
|
MG_Impl::GLImpl::BufferData(GL_ATOMIC_COUNTER_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
||||||
|
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 2, buffer);
|
||||||
|
MG_Impl::GLImpl::GetIntegerv(GL_ATOMIC_COUNTER_BUFFER_BINDING, &reported);
|
||||||
|
EXPECT_EQ(static_cast<GLuint>(reported), buffer);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// The advertised ceiling is also the one glBindBufferBase and the indexed getter enforce.
|
||||||
|
// A limit nothing validates against is how these tables drifted apart in the first place:
|
||||||
|
// the binding-point ARRAY is 36 deep, and it used to be that number an application saw.
|
||||||
|
constexpr GLuint pastLastBinding = static_cast<GLuint>(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
|
||||||
|
MG_Impl::GLImpl::BindBufferBase(GL_ATOMIC_COUNTER_BUFFER, pastLastBinding, buffer);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_ATOMIC_COUNTER_BUFFER_BINDING, pastLastBinding, &reported);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
|
||||||
|
|
||||||
|
// ...and the shading language has to expand the same numbers. Each array is sized by a
|
||||||
|
// built-in constant and indexed at its last element with a literal, so the stage only
|
||||||
|
// compiles when that constant is at least what glGetIntegerv just reported - which it was
|
||||||
|
// not while the resource table said one.
|
||||||
|
const String lastBinding = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS - 1);
|
||||||
|
const String lastBuffer = std::to_string(Transpiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE - 1);
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
out vec4 color;
|
||||||
|
int mgBindings[gl_MaxAtomicCounterBindings];
|
||||||
|
int mgCombinedBuffers[gl_MaxCombinedAtomicCounterBuffers];
|
||||||
|
int mgFragmentBuffers[gl_MaxFragmentAtomicCounterBuffers];
|
||||||
|
layout(binding = )" + lastBinding + R"(, offset = 0) uniform atomic_uint mgCounter;
|
||||||
|
void main() {
|
||||||
|
color = vec4(float(mgBindings[)" + lastBinding + R"(] + mgCombinedBuffers[)" + lastBuffer +
|
||||||
|
R"(] + mgFragmentBuffers[)" + lastBuffer + R"(] + int(atomicCounterIncrement(mgCounter))));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_FRAGMENT_SHADER,
|
||||||
|
.sourceStr = source,
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
|
||||||
|
|
||||||
|
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||||
|
MG_State::pGLContext = Move(previousContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.compute_shader.max: the test queries every GL_MAX_COMPUTE_* value through the API and
|
||||||
|
// then makes a compute shader compare the matching gl_MaxCompute* constant against it. The two
|
||||||
|
// used to be independent tables and gl_MaxComputeWorkGroupSize.z disagreed - glslang compiled
|
||||||
|
// against a permissive 1024 while the context advertises the 64 the GL 4.6 minimum (and every ES
|
||||||
|
// driver) reports.
|
||||||
|
TEST(GetterSanity, ComputeWorkGroupQueriesMatchShaderCompilerLimits) {
|
||||||
|
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{});
|
||||||
|
|
||||||
|
GLint size[3] = {0, 0, 0};
|
||||||
|
GLint count[3] = {0, 0, 0};
|
||||||
|
for (GLuint index = 0; index < 3; ++index) {
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, &size[index]);
|
||||||
|
MG_Impl::GLImpl::GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index, &count[index]);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// The compile runs against a captured env, exactly as the pipeline's does. That is the whole
|
||||||
|
// invariant: the env holds the same floored driver answer GetIntegeri_v just returned, so the
|
||||||
|
// resource table and the query agree BY CONSTRUCTION rather than by two tables happening to
|
||||||
|
// carry the same literals.
|
||||||
|
const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv();
|
||||||
|
for (GLuint index = 0; index < 3; ++index) {
|
||||||
|
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupSize[index]), size[index]) << "index " << index;
|
||||||
|
EXPECT_EQ(static_cast<GLint>(env->maxComputeWorkGroupCount[index]), count[index]) << "index " << index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A negative array size is a compile error, so the stage only compiles when EVERY component
|
||||||
|
// of both built-in constants equals what the query above reported. Two-sided by construction:
|
||||||
|
// a resource table that is too permissive fails it exactly like one that is too tight.
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
const int mgAgree = (gl_MaxComputeWorkGroupSize == ivec3()" +
|
||||||
|
std::to_string(size[0]) + ", " + std::to_string(size[1]) + ", " +
|
||||||
|
std::to_string(size[2]) + R"() &&
|
||||||
|
gl_MaxComputeWorkGroupCount == ivec3()" +
|
||||||
|
std::to_string(count[0]) + ", " + std::to_string(count[1]) + ", " +
|
||||||
|
std::to_string(count[2]) + R"()) ? 1 : -1;
|
||||||
|
int mgProbe[mgAgree];
|
||||||
|
void main() {
|
||||||
|
mgProbe[0] = 0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_COMPUTE_SHADER,
|
||||||
|
.sourceStr = source,
|
||||||
|
.env = env.get(),
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(compiled) << (compiled ? "" : compiled.error().log);
|
||||||
|
|
||||||
|
// The z ceiling is also what glslang checks a declared local_size_z against, so it has to
|
||||||
|
// reject one invocation past the advertised limit and accept the limit itself.
|
||||||
|
const String atLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2]) +
|
||||||
|
") in;\nvoid main() {}\n";
|
||||||
|
const String pastLimit = "#version 430 core\nlayout(local_size_z = " + std::to_string(size[2] + 1) +
|
||||||
|
") in;\nvoid main() {}\n";
|
||||||
|
EXPECT_TRUE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_COMPUTE_SHADER,
|
||||||
|
.sourceStr = atLimit,
|
||||||
|
.env = env.get(),
|
||||||
|
}));
|
||||||
|
EXPECT_FALSE(MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
|
||||||
|
.shaderType = GL_COMPUTE_SHADER,
|
||||||
|
.sourceStr = pastLimit,
|
||||||
|
.env = env.get(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
MG_Backend::pActiveBackendObject = Move(previousBackend);
|
||||||
|
MG_State::pGLContext = Move(previousContext);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ add_executable(
|
|||||||
EmulateSubgroupsTest.cpp
|
EmulateSubgroupsTest.cpp
|
||||||
DemoteFloat64Test.cpp
|
DemoteFloat64Test.cpp
|
||||||
FlattenXfbInterfaceBlocksTest.cpp
|
FlattenXfbInterfaceBlocksTest.cpp
|
||||||
|
UniquifyIoBlockNamesTest.cpp
|
||||||
LowerViewportIndexTest.cpp
|
LowerViewportIndexTest.cpp
|
||||||
ClampMultisampleFetchTest.cpp
|
ClampMultisampleFetchTest.cpp
|
||||||
)
|
)
|
||||||
@@ -29,5 +30,33 @@ if (MSVC)
|
|||||||
target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor)
|
target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Its own binary: several cases flip MG_Config::Features.ShaderTranslationCache and
|
||||||
|
# MG_Config::Features.AsyncShaderCompile and read the PROCESS-GLOBAL L1 counters straight
|
||||||
|
# afterwards, so another suite's links running in the same process would show up as
|
||||||
|
# hits/misses this one did not cause.
|
||||||
|
add_executable(
|
||||||
|
TranslationCacheTest
|
||||||
|
TranslationCacheTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(TranslationCacheTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
TranslationCacheTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(TranslationCacheTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
# Heavier than a pure-pass suite: the concurrency cases run 8 threads over thousands of
|
||||||
|
# rounds and the end-to-end cases link real programs through the compile pool.
|
||||||
|
gtest_discover_tests(TranslationCacheTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/UniquifyIoBlockNamesTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <spirv-tools/libspirv.hpp>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||||
|
if (!shaderResult) return {};
|
||||||
|
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||||
|
if (!programResult) return {};
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||||
|
if (!binaryResult || binaryResult->empty()) return {};
|
||||||
|
return binaryResult->front();
|
||||||
|
}
|
||||||
|
|
||||||
|
String Disassemble(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String text;
|
||||||
|
tools.Disassemble(spirv, &text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Transpile(const Vector<Uint32>& spirv) {
|
||||||
|
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||||
|
return essl ? essl.value() : String{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tessellation evaluation stage of
|
||||||
|
// KHR-GL42/43.shading_language_420pack.length_of_vector_and_matrix_* and
|
||||||
|
// .qualifier_order_block_*, reduced to the shape that matters: ONE block name used for
|
||||||
|
// both the block this stage consumes and the block it produces. Legal desktop GLSL - the
|
||||||
|
// input and output block namespaces are separate - and something SPIRV-Cross re-emits
|
||||||
|
// verbatim, so the ESSL it produces declares two different blocks called TCSOutputBlock.
|
||||||
|
const char* kCollidingTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
|
||||||
|
in vec4 tcs_tes_result[];
|
||||||
|
out vec4 tes_gs_result;
|
||||||
|
|
||||||
|
in TCSOutputBlock {
|
||||||
|
vec4 tcs_tes_variable;
|
||||||
|
} input_block[];
|
||||||
|
out TCSOutputBlock {
|
||||||
|
vec4 tes_gs_variable;
|
||||||
|
} output_block;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_gs_result = tcs_tes_result[0];
|
||||||
|
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The same stage with the two blocks already named apart, which is the overwhelmingly
|
||||||
|
// common shape and the one that must go through untouched.
|
||||||
|
const char* kDistinctTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
|
||||||
|
in vec4 tcs_tes_result[];
|
||||||
|
out vec4 tes_gs_result;
|
||||||
|
|
||||||
|
in TCSOutputBlock {
|
||||||
|
vec4 tcs_tes_variable;
|
||||||
|
} input_block[];
|
||||||
|
out TESOutputBlock {
|
||||||
|
vec4 tes_gs_variable;
|
||||||
|
} output_block;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
tes_gs_result = tcs_tes_result[0];
|
||||||
|
output_block.tes_gs_variable = input_block[0].tcs_tes_variable;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// gl_PerVertex is an Input block AND an Output block of one name in every tessellation
|
||||||
|
// and geometry stage. It is the language's block, not the shader's, so it must never be
|
||||||
|
// reported and never be renamed.
|
||||||
|
const char* kBuiltinBlockOnlyTessEvalSource = R"(#version 420 core
|
||||||
|
layout(isolines, point_mode) in;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class UniquifyIoBlockNamesTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
MobileGL::Initialize();
|
||||||
|
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||||
|
<< "the renamed module did not survive spirv-val";
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 m_validationFailuresAtStart = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, ProbeReportsABlockNameUsedInBothDirections) {
|
||||||
|
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
std::set<String> colliding;
|
||||||
|
std::set<String> declared;
|
||||||
|
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||||
|
|
||||||
|
EXPECT_EQ(colliding, (std::set<String>{"TCSOutputBlock"}));
|
||||||
|
// The name set the caller picks a replacement out of has to contain what the module
|
||||||
|
// already spells, or the replacement could land on top of an existing declaration.
|
||||||
|
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
|
||||||
|
EXPECT_NE(declared.find("input_block"), declared.end());
|
||||||
|
EXPECT_NE(declared.find("output_block"), declared.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, ProbeIgnoresAStageWhoseBlocksAlreadyHaveDistinctNames) {
|
||||||
|
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
std::set<String> colliding;
|
||||||
|
std::set<String> declared;
|
||||||
|
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||||
|
|
||||||
|
EXPECT_TRUE(colliding.empty());
|
||||||
|
EXPECT_NE(declared.find("TCSOutputBlock"), declared.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, ProbeNeverReportsTheBuiltinBlock) {
|
||||||
|
const Vector<Uint32> input =
|
||||||
|
CompileToSpirv(GL_TESS_EVALUATION_SHADER, kBuiltinBlockOnlyTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
std::set<String> colliding;
|
||||||
|
std::set<String> declared;
|
||||||
|
ShaderCompiler::ProbeIoBlockNamesForEssl(input, colliding, declared);
|
||||||
|
|
||||||
|
// gl_PerVertex is read through gl_in and written through gl_Position, i.e. it is exactly
|
||||||
|
// the in-and-out-under-one-name shape - and renaming it would invent a block no driver
|
||||||
|
// knows.
|
||||||
|
EXPECT_TRUE(colliding.empty()) << "gl_PerVertex must never enter the rename plan";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, RenamesTheTwoBlocksApartInTheEmittedEssl) {
|
||||||
|
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
// The generated ESSL really does declare the block twice under one name before the fix -
|
||||||
|
// pinning the defect, not just the repair.
|
||||||
|
const String before = Transpile(input);
|
||||||
|
EXPECT_NE(before.find("in TCSOutputBlock"), String::npos) << before;
|
||||||
|
EXPECT_NE(before.find("out TCSOutputBlock"), String::npos) << before;
|
||||||
|
|
||||||
|
// The plan the DirectGLES program build makes for a five-stage program: what this stage
|
||||||
|
// consumes is spelled after the tessellation control stage (pipeline index 1) and what it
|
||||||
|
// produces after itself (pipeline index 2).
|
||||||
|
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
|
||||||
|
const std::map<String, String> outputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio2"}};
|
||||||
|
|
||||||
|
std::set<String> renamed;
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, outputRenames, renamed,
|
||||||
|
output, true));
|
||||||
|
ASSERT_FALSE(output.empty());
|
||||||
|
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
|
||||||
|
|
||||||
|
const String dis = Disassemble(output);
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||||
|
EXPECT_EQ(dis.find("\"TCSOutputBlock\""), String::npos)
|
||||||
|
<< "the colliding name is still on a block struct:\n"
|
||||||
|
<< dis;
|
||||||
|
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
|
||||||
|
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
|
||||||
|
|
||||||
|
const String after = Transpile(output);
|
||||||
|
EXPECT_NE(after.find("TCSOutputBlock_mgio1"), String::npos) << after;
|
||||||
|
EXPECT_NE(after.find("TCSOutputBlock_mgio2"), String::npos) << after;
|
||||||
|
// Only the block TYPE name moves: the instance names are what the body reads and writes
|
||||||
|
// through, and the member names are half of what ES matches the interface by.
|
||||||
|
EXPECT_NE(after.find("input_block"), String::npos) << after;
|
||||||
|
EXPECT_NE(after.find("output_block"), String::npos) << after;
|
||||||
|
EXPECT_NE(after.find("tcs_tes_variable"), String::npos) << after;
|
||||||
|
EXPECT_NE(after.find("tes_gs_variable"), String::npos) << after;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, RenamesOnlyTheDirectionTheCallerPlanned) {
|
||||||
|
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kCollidingTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
// A separate-shader-objects program that ends at this stage plans no output rename,
|
||||||
|
// because the block's consumer lives in another program that never saw the plan.
|
||||||
|
const std::map<String, String> inputRenames{{"TCSOutputBlock", "TCSOutputBlock_mgio1"}};
|
||||||
|
|
||||||
|
std::set<String> renamed;
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(
|
||||||
|
ShaderCompiler::UniquifyIoBlockNamesForEssl(input, inputRenames, {}, renamed, output, true));
|
||||||
|
ASSERT_FALSE(output.empty());
|
||||||
|
EXPECT_EQ(renamed, (std::set<String>{"TCSOutputBlock"}));
|
||||||
|
|
||||||
|
const String dis = Disassemble(output);
|
||||||
|
EXPECT_NE(dis.find("\"TCSOutputBlock_mgio1\""), String::npos) << dis;
|
||||||
|
// The output block keeps the name the other program still spells.
|
||||||
|
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
|
||||||
|
EXPECT_EQ(dis.find("\"TCSOutputBlock_mgio2\""), String::npos) << dis;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UniquifyIoBlockNamesTest, ReportsNothingWhenThePlanNamesNoBlockThisStageDeclares) {
|
||||||
|
const Vector<Uint32> input = CompileToSpirv(GL_TESS_EVALUATION_SHADER, kDistinctTessEvalSource);
|
||||||
|
ASSERT_FALSE(input.empty());
|
||||||
|
|
||||||
|
const std::map<String, String> renames{{"SomeOtherBlock", "SomeOtherBlock_mgio2"}};
|
||||||
|
|
||||||
|
std::set<String> renamed;
|
||||||
|
Vector<Uint32> output;
|
||||||
|
ASSERT_TRUE(ShaderCompiler::UniquifyIoBlockNamesForEssl(input, renames, renames, renamed, output, true));
|
||||||
|
// Empty is what tells the DirectGLES program build to keep the module it already had
|
||||||
|
// instead of adopting the optimizer's re-serialised copy.
|
||||||
|
EXPECT_TRUE(renamed.empty());
|
||||||
|
|
||||||
|
const String dis = Disassemble(output);
|
||||||
|
EXPECT_NE(dis.find("\"TCSOutputBlock\""), String::npos) << dis;
|
||||||
|
EXPECT_NE(dis.find("\"TESOutputBlock\""), String::npos) << dis;
|
||||||
|
}
|
||||||
@@ -3197,11 +3197,19 @@ TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
|
|||||||
GLenum type;
|
GLenum type;
|
||||||
};
|
};
|
||||||
const Case cases[] = {
|
const Case cases[] = {
|
||||||
// Legacy <=8-bit-per-channel formats store as UNorm8 component arrays.
|
// Legacy <=8-bit-per-channel DESKTOP-ONLY formats store as UNorm8 component arrays, in the
|
||||||
{GL_R3_G3_B2, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
// 8-bit-per-channel ES format that layout already is. Storing them in the narrower
|
||||||
{GL_RGB4, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
// GL_RGB565/GL_RGBA4 they nominally fit in made the driver requantize the shadow bytes on
|
||||||
{GL_RGB5, GL_RGB565, GL_RGB, GL_UNSIGNED_BYTE},
|
// every upload, which is not lossless: 5-bit 2 -> UNorm8 16 -> 16/255*31 = 1.945, which a
|
||||||
{GL_RGBA2, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
|
// truncating driver reads back as 1 (KHR-GL43.copy_image rgb4->rgb4, 12/12 failing on Mali).
|
||||||
|
{GL_R3_G3_B2, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||||
|
{GL_RGB4, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||||
|
{GL_RGB5, GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE},
|
||||||
|
{GL_RGBA2, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||||
|
// The two that are ES formats in their own right keep their native storage: an application
|
||||||
|
// that asks for GL_RGBA4 or GL_RGB5_A1 is asking for the smaller image, and the same
|
||||||
|
// normalization also picks the storage for glRenderbufferStorage, where those two are
|
||||||
|
// ordinary ES render targets rather than a desktop-compatibility shim.
|
||||||
{GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
|
{GL_RGBA4, GL_RGBA4, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||||
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE},
|
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_BYTE},
|
||||||
// 10/12-bit channels store as UNorm16 component arrays.
|
// 10/12-bit channels store as UNorm16 component arrays.
|
||||||
@@ -3860,6 +3868,131 @@ TEST_F(TextureTest, ColorAttachableTargetsRequestTheThreeChannelWidening) {
|
|||||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||||
|
|
||||||
|
// ...and neither can an 8-bit one. That half of the answer used to be missing entirely, which
|
||||||
|
// is why an R8_SNORM / RG8_SNORM colour attachment got no substitute at all on a driver
|
||||||
|
// without EXT_render_snorm.
|
||||||
|
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noSnormCapabilities, texture2DIndex) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||||
|
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||||
|
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noSnormCapabilities, bufferIndex) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||||
|
|
||||||
|
// 8-bit signed-normalized storage is core ES, so only EXT_render_snorm gates the 8-bit bit;
|
||||||
|
// the 16-bit one also needs EXT_texture_norm16 for the encoding to exist at all.
|
||||||
|
MG_External::GLESCapabilities noNorm16Capabilities{};
|
||||||
|
noNorm16Capabilities.SupportsRenderSnorm = true;
|
||||||
|
noNorm16Capabilities.SupportsNorm16Texture = false;
|
||||||
|
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||||
|
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Signed-normalized colour-renderable substitution (KHR-GL4x.texture_swizzle on Mali) -------
|
||||||
|
//
|
||||||
|
// A driver without GL_EXT_render_snorm treats every signed-normalized format as texture-only, so a
|
||||||
|
// colour attachment in one of them leaves the ES framebuffer incomplete: the draw lands nowhere and
|
||||||
|
// the readback falls through to the CPU shadow, which for a glTexImage2D(..., nullptr) output
|
||||||
|
// texture is all zeroes. The render-target bits used to reach GL_RGB16_SNORM alone, so five of the
|
||||||
|
// eight SNORM formats - and in particular the single-channel GL_R8_SNORM / GL_R16_SNORM that
|
||||||
|
// KHR-GL4x.texture_swizzle renders into for EVERY SNORM source format - had no fallback at all.
|
||||||
|
|
||||||
|
TEST_F(TextureTest, SnormRenderTargetOptionsApplyToEverySignedNormalizedFormat) {
|
||||||
|
using MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions;
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> requested =
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget | PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
|
|
||||||
|
for (const GLenum internalFormat : {GL_R8_SNORM, GL_RG8_SNORM, GL_RGB8_SNORM, GL_RGBA8_SNORM}) {
|
||||||
|
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||||
|
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||||
|
<< "internalformat 0x" << std::hex << internalFormat;
|
||||||
|
// The two bits are per precision class, so the 16-bit one never reaches an 8-bit format -
|
||||||
|
// that is what keeps the fallback reason from naming both.
|
||||||
|
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||||
|
<< "internalformat 0x" << std::hex << internalFormat;
|
||||||
|
}
|
||||||
|
for (const GLenum internalFormat : {GL_R16_SNORM, GL_RG16_SNORM, GL_RGB16_SNORM, GL_RGBA16_SNORM}) {
|
||||||
|
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||||
|
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||||
|
<< "internalformat 0x" << std::hex << internalFormat;
|
||||||
|
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||||
|
<< "internalformat 0x" << std::hex << internalFormat;
|
||||||
|
}
|
||||||
|
// GL_RGB16_SNORM used to be granted the 16-bit bit only when the three-channel widening was
|
||||||
|
// requested alongside it, which made the answer depend on the order the caller assembled its
|
||||||
|
// option set in. The capability probe and the runtime storage choice assemble different sets.
|
||||||
|
EXPECT_TRUE(GetApplicablePixelFormatNormalizeOptions(GL_RGB16_SNORM,
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||||
|
|
||||||
|
// Nothing else responds to either bit; an unsigned-normalized or float format keeps its storage.
|
||||||
|
for (const GLenum internalFormat : {GL_R8, GL_R16, GL_RGBA8, GL_RGBA16, GL_RGB16F, GL_RGBA32F, GL_RGB9_E5}) {
|
||||||
|
EXPECT_FALSE(GetApplicablePixelFormatNormalizeOptions(internalFormat, requested))
|
||||||
|
<< "internalformat 0x" << std::hex << internalFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TextureTest, SnormRenderTargetSubstitutesKeepEveryChannelValueExactly) {
|
||||||
|
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
|
||||||
|
struct Case {
|
||||||
|
GLenum requested;
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
|
GLenum internalFormat;
|
||||||
|
GLenum format;
|
||||||
|
GLenum type;
|
||||||
|
};
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> snorm8RT = PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> snorm16RT = PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
|
|
||||||
|
const Case cases[] = {
|
||||||
|
// 8-bit: a half float represents every v/127 exactly (the worst case, -123/127, quantizes
|
||||||
|
// 0.03 of a SNORM step away), so it is the same storage GL_RGBA8_SNORM already always got.
|
||||||
|
{GL_R8_SNORM, snorm8RT, GL_R16F, GL_RED, GL_FLOAT},
|
||||||
|
{GL_RG8_SNORM, snorm8RT, GL_RG16F, GL_RG, GL_FLOAT},
|
||||||
|
{GL_RGBA8_SNORM, snorm8RT, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
||||||
|
// 16-bit: NOT a half float. Its spacing just below 1.0 is some 16 SNORM steps, so it hands
|
||||||
|
// -23451/32767 back as -23457 against a conformance window of one step; a 32-bit float
|
||||||
|
// round-trips all 65535 channel values.
|
||||||
|
{GL_R16_SNORM, snorm16RT, GL_R32F, GL_RED, GL_FLOAT},
|
||||||
|
{GL_RG16_SNORM, snorm16RT, GL_RG32F, GL_RG, GL_FLOAT},
|
||||||
|
{GL_RGBA16_SNORM, snorm16RT, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
|
// The render-target bit outranks the narrower fallbacks, whichever way the caller's option
|
||||||
|
// set was assembled: the capability probe folds the driver options in, the runtime storage
|
||||||
|
// choice can see the render-target bit alone, and the two have to pick the same storage.
|
||||||
|
{GL_R16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16, GL_R32F, GL_RED, GL_FLOAT},
|
||||||
|
{GL_RG16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoSnorm16, GL_RG32F, GL_RG, GL_FLOAT},
|
||||||
|
{GL_RGBA16_SNORM,
|
||||||
|
snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16 | PixelFormatNormalizeOptionBit::NoSnorm16,
|
||||||
|
GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
|
// The three-channel formats go on through the widening, which outranks everything.
|
||||||
|
{GL_RGB8_SNORM, snorm8RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA16F, GL_RGBA,
|
||||||
|
GL_FLOAT},
|
||||||
|
{GL_RGB16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA32F, GL_RGBA,
|
||||||
|
GL_FLOAT},
|
||||||
|
// Control: with EXT_render_snorm neither bit is ever set, so the driver that renders to the
|
||||||
|
// signed-normalized encoding keeps storing it byte for byte. This is the shape Adreno and
|
||||||
|
// llvmpipe take, which is why the substitution is invisible on every gate the project runs.
|
||||||
|
{GL_R8_SNORM, PixelFormatNormalizeOptionBit::None, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||||
|
{GL_RG8_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG8_SNORM, GL_RG, GL_BYTE},
|
||||||
|
{GL_R16_SNORM, PixelFormatNormalizeOptionBit::None, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||||
|
{GL_RG16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG16_SNORM, GL_RG, GL_SHORT},
|
||||||
|
{GL_RGBA16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||||
|
// ...and the bit for the other precision class does nothing on its own.
|
||||||
|
{GL_R8_SNORM, snorm16RT, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||||
|
{GL_R16_SNORM, snorm8RT, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& testCase : cases) {
|
||||||
|
GLenum internalFormat = 0;
|
||||||
|
GLenum format = 0;
|
||||||
|
GLenum type = 0;
|
||||||
|
NormalizePixelFormat(testCase.requested, testCase.options, &internalFormat, &format, &type);
|
||||||
|
EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested;
|
||||||
|
EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested;
|
||||||
|
EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(TextureTest, ThreeChannelRenderTargetOptionAppliesToEveryDeniedThreeChannelFormat) {
|
TEST_F(TextureTest, ThreeChannelRenderTargetOptionAppliesToEveryDeniedThreeChannelFormat) {
|
||||||
@@ -3910,9 +4043,10 @@ TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTo
|
|||||||
{GL_RGB16F, widen, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT},
|
{GL_RGB16F, widen, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT},
|
||||||
{GL_RGB32F, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
{GL_RGB32F, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
// 16-bit SNORM keeps its encoding where EXT_render_snorm can render to it; a half float's
|
// 16-bit SNORM keeps its encoding where EXT_render_snorm can render to it; a half float's
|
||||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly, so the driver that
|
||||||
|
// cannot render to the encoding gets the 32-bit float rather than the half.
|
||||||
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
|
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
|
||||||
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||||
@@ -4194,24 +4328,30 @@ TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) {
|
|||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) {
|
TEST_F(TextureTest, GetTexLevelParameterAnswersBufferStorageGeometry) {
|
||||||
// TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap
|
// TextureStorageType is {Mipmap, Buffer} and the level queries used to answer only out of a
|
||||||
// chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
// mipmap chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a
|
||||||
// THROW_UNIMPL_EXCEPTION default: label and killed the process.
|
// THROW_UNIMPL_EXCEPTION default: label and killed the process. It now answers out of the
|
||||||
|
// attached buffer range instead (GL 4.6 core 8.9): a buffer texture is one-dimensional, and
|
||||||
|
// with no buffer attached it addresses no texels at all.
|
||||||
GLuint texture = 0;
|
GLuint texture = 0;
|
||||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture);
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture);
|
||||||
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
|
MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0);
|
||||||
DrainPendingGlErrors();
|
DrainPendingGlErrors();
|
||||||
|
|
||||||
for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) {
|
const std::pair<GLenum, GLint> expectations[] = {
|
||||||
|
{GL_TEXTURE_WIDTH, 0}, {GL_TEXTURE_HEIGHT, 1}, {GL_TEXTURE_DEPTH, 1}};
|
||||||
|
for (const auto& [pname, expected] : expectations) {
|
||||||
GLint intParam = 0x20202020;
|
GLint intParam = 0x20202020;
|
||||||
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
|
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam);
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(intParam, expected) << "pname " << pname;
|
||||||
|
|
||||||
GLfloat floatParam = 12345.0f;
|
GLfloat floatParam = 12345.0f;
|
||||||
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
|
MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam);
|
||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
EXPECT_EQ(floatParam, static_cast<GLfloat>(expected)) << "pname " << pname;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4676,6 +4816,208 @@ TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) {
|
|||||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's boundaries, and
|
||||||
|
// this validator had no bounds check whatsoever: the one call shaped like one,
|
||||||
|
// ValidateCopyImageBlockAlignment, returns true on its first line for every UNCOMPRESSED format.
|
||||||
|
// Texture endpoints only looked covered because the ES driver raised its own error - which
|
||||||
|
// DirectGLES logs and swallows, so the application saw GL_NO_ERROR and a destination that never
|
||||||
|
// changed (KHR-GL43.copy_image.exceeding_boundaries).
|
||||||
|
TEST_F(TextureTest, CopyImageSubDataRejectsARegionThatLeavesTheImage) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
|
||||||
|
GLuint srcTexture = 0;
|
||||||
|
GLuint dstTexture = 0;
|
||||||
|
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// The region that exactly reaches the far edge is the boundary this must NOT reject - a
|
||||||
|
// validator that answered INVALID_VALUE to every non-origin region would satisfy the negatives
|
||||||
|
// below and break every legal partial copy.
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 4, 4, 0, dstTexture, GL_TEXTURE_2D, 0, 4, 4, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// One texel past it on x, on y, and on the destination side.
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 5, 4, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 4, 5, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 5, 5, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// A negative origin is out of bounds on the other side of the same rule.
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, -1, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The endpoint the missing bounds check actually cost: a renderbuffer never reaches the ES
|
||||||
|
// driver's texture-shaped checks either, so a 4x4 region at y = 14 of a 16x16 renderbuffer - the
|
||||||
|
// exact sub-case KHR-GL43.copy_image.exceeding_boundaries starts with, GL_RENDERBUFFER being first
|
||||||
|
// in its target list - was accepted outright.
|
||||||
|
TEST_F(TextureTest, CopyImageSubDataBoundsARenderbufferRegion) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 16, 16);
|
||||||
|
GLuint renderbuffer = 0;
|
||||||
|
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
|
||||||
|
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 16, 16);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 12, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 14, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// ...and as the destination, where the same renderbuffer has the same one image.
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 14, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
// A renderbuffer has exactly one slice, so any z at all is out of range.
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 1, texture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
|
4, 4, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The z axis was structurally unbounded - srcZ/dstZ did not even reach the validator - so a layer
|
||||||
|
// range running off the end of an array reached the backend as an out-of-range image subresource.
|
||||||
|
TEST_F(TextureTest, CopyImageSubDataBoundsTheLayerRangeOfAnArray) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
|
||||||
|
GLuint srcTexture = 0;
|
||||||
|
GLuint dstTexture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
|
||||||
|
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||||
|
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||||
|
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// Layers 5..11 of a 12-layer array: the last one the range may reach.
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 5, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||||
|
0, 0, 0, 5, 4, 4, 7);
|
||||||
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 6, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||||
|
0, 0, 0, 0, 4, 4, 7);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||||
|
0, 0, 0, 6, 4, 4, 7);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The convention the bounds check has to get right, and the one that would silently reject legal
|
||||||
|
// copies if it did not: on a CUBE MAP the z axis selects among the six faces, which this frontend
|
||||||
|
// keeps as six separate one-slice upload targets - so the level's own extent reports depth 1 and a
|
||||||
|
// bound taken from it would refuse every whole-cube copy.
|
||||||
|
TEST_F(TextureTest, CopyImageSubDataCountsCubeMapFacesOnTheZAxis) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
|
||||||
|
GLuint srcTexture = 0;
|
||||||
|
GLuint dstTexture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &srcTexture);
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_CUBE_MAP, 1, &dstTexture);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
|
||||||
|
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcTexture);
|
||||||
|
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstTexture);
|
||||||
|
ASSERT_NE(srcObject, nullptr);
|
||||||
|
ASSERT_NE(dstObject, nullptr);
|
||||||
|
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||||
|
GTEST_SKIP() << "this context could not give the cube maps storage";
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP, 0, 0, 0, 0, dstTexture, GL_TEXTURE_CUBE_MAP,
|
||||||
|
0, 0, 0, 0, 8, 8, 6);
|
||||||
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
// A seventh face does not exist.
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP, 0, 0, 0, 1, dstTexture, GL_TEXTURE_CUBE_MAP,
|
||||||
|
0, 0, 0, 0, 8, 8, 6);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other axis convention: GL puts a 1D ARRAY's layers on y for this entry point (srcY is the
|
||||||
|
// first layer, srcHeight the layer count), which is also where this frontend keeps them - so the
|
||||||
|
// level extent answers directly and z stays a single slice.
|
||||||
|
TEST_F(TextureTest, CopyImageSubDataBoundsA1DArraysLayersOnTheYAxis) {
|
||||||
|
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
|
||||||
|
GLuint srcTexture = 0;
|
||||||
|
GLuint dstTexture = 0;
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_1D_ARRAY, 1, &srcTexture);
|
||||||
|
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_1D_ARRAY, 1, &dstTexture);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 16, 8);
|
||||||
|
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 16, 8);
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
|
||||||
|
const auto srcObject = MG_State::pGLContext->GetTextureObject(srcTexture);
|
||||||
|
const auto dstObject = MG_State::pGLContext->GetTextureObject(dstTexture);
|
||||||
|
ASSERT_NE(srcObject, nullptr);
|
||||||
|
ASSERT_NE(dstObject, nullptr);
|
||||||
|
if (!srcObject->IsComplete() || !dstObject->IsComplete()) {
|
||||||
|
GTEST_SKIP() << "this context could not give the 1D arrays storage";
|
||||||
|
}
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 3, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
|
||||||
|
0, 0, 3, 0, 4, 5, 1);
|
||||||
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
g_copyImageSubDataCall = {};
|
||||||
|
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 4, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
|
||||||
|
0, 0, 0, 0, 4, 5, 1);
|
||||||
|
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||||
|
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
|
// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core
|
||||||
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
|
// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no
|
||||||
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
|
// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2
|
||||||
@@ -4809,3 +5151,41 @@ TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleT
|
|||||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 8.11 makes GL_IMAGE_FORMAT_COMPATIBILITY_TYPE readable through every
|
||||||
|
// GetTexParameter form. Three of MobileGL's four getters answered it and glGetTexParameterfv did
|
||||||
|
// not, so the float query raised GL_INVALID_ENUM and left the caller's float uninitialised
|
||||||
|
// (KHR-GL4x.shader_image_load_store.basic-api-texParam reads it with both iv and fv and compares
|
||||||
|
// them). Asserted across all four here, because an enum present in three of four parallel
|
||||||
|
// switches is the drift shape that comes back.
|
||||||
|
TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGetter) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
|
||||||
|
GLint integerValue = 0;
|
||||||
|
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &integerValue);
|
||||||
|
EXPECT_EQ(integerValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLfloat floatValue = 0.0f;
|
||||||
|
MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &floatValue);
|
||||||
|
EXPECT_FLOAT_EQ(floatValue, static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLint signedValue = 0;
|
||||||
|
MG_Impl::GLImpl::GetTexParameterIiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &signedValue);
|
||||||
|
EXPECT_EQ(signedValue, GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
GLuint unsignedValue = 0;
|
||||||
|
MG_Impl::GLImpl::GetTexParameterIuiv(GL_TEXTURE_2D, GL_IMAGE_FORMAT_COMPATIBILITY_TYPE, &unsignedValue);
|
||||||
|
EXPECT_EQ(unsignedValue, static_cast<GLuint>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE));
|
||||||
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
|
|
||||||
|
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
||||||
|
DrainPendingGlErrors();
|
||||||
|
}
|
||||||
|
|||||||
@@ -831,6 +831,35 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
return includesBase;
|
return includesBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL 4.6 table 23.65 admits exactly four answers for GL_LAYER_PROVOKING_VERTEX and
|
||||||
|
// GL_VIEWPORT_INDEX_PROVOKING_VERTEX. Anything else means the driver wrote something MobileGL
|
||||||
|
// cannot forward as a convention, and GL_UNDEFINED_VERTEX - a legal answer, not a placeholder
|
||||||
|
// - is the accurate thing to say about it.
|
||||||
|
static GLenum NormalizeProvokingVertexConvention(GLint driverValue) {
|
||||||
|
switch (static_cast<GLenum>(driverValue)) {
|
||||||
|
case GL_FIRST_VERTEX_CONVENTION:
|
||||||
|
case GL_LAST_VERTEX_CONVENTION:
|
||||||
|
case GL_PROVOKING_VERTEX:
|
||||||
|
case GL_UNDEFINED_VERTEX:
|
||||||
|
return static_cast<GLenum>(driverValue);
|
||||||
|
default:
|
||||||
|
return GL_UNDEFINED_VERTEX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* ProvokingVertexConventionName(GLenum convention) {
|
||||||
|
switch (convention) {
|
||||||
|
case GL_FIRST_VERTEX_CONVENTION:
|
||||||
|
return "GL_FIRST_VERTEX_CONVENTION";
|
||||||
|
case GL_LAST_VERTEX_CONVENTION:
|
||||||
|
return "GL_LAST_VERTEX_CONVENTION";
|
||||||
|
case GL_PROVOKING_VERTEX:
|
||||||
|
return "GL_PROVOKING_VERTEX";
|
||||||
|
default:
|
||||||
|
return "GL_UNDEFINED_VERTEX";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) {
|
Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||||
if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) {
|
if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) {
|
||||||
MGLOG_E("Required GLES functions are not loaded, cannot query capabilities");
|
MGLOG_E("Required GLES functions are not loaded, cannot query capabilities");
|
||||||
@@ -1063,8 +1092,21 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLint maxComputeImageUniforms = 8;
|
GLint maxComputeImageUniforms = 8;
|
||||||
GLint maxDrawBuffers = 8;
|
GLint maxDrawBuffers = 8;
|
||||||
GLint maxColorAttachments = 8;
|
GLint maxColorAttachments = 8;
|
||||||
GLint maxClipDistances = 8;
|
// Zero is a legal answer, not a placeholder. GL_MAX_CLIP_DISTANCES exists in ES only as
|
||||||
|
// GL_MAX_CLIP_DISTANCES_EXT under GL_EXT_clip_cull_distance, so on a driver without that
|
||||||
|
// extension there is nowhere to put a clip distance at all: SPIRV-Cross emits
|
||||||
|
// gl_ClipDistance behind an `#extension ... : require` the ESSL compiler rejects, and
|
||||||
|
// DirectGLES has no state to forward the per-distance enables into (see the gate in
|
||||||
|
// DirectGLES::SyncRenderState). Starting at 8 meant a probe that could never run left an
|
||||||
|
// 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;
|
||||||
GLint maxViewports = 16;
|
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
|
||||||
|
// there is no layered or multi-viewport draw for a convention to describe.
|
||||||
|
GLenum layerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
GLenum viewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
GLfloat minFragmentInterpolationOffset = -0.5f;
|
GLfloat minFragmentInterpolationOffset = -0.5f;
|
||||||
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
GLfloat maxFragmentInterpolationOffset = 0.4375f;
|
||||||
GLint fragmentInterpolationOffsetBits = 4;
|
GLint fragmentInterpolationOffsetBits = 4;
|
||||||
@@ -1074,11 +1116,39 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLint maxProgramTextureGatherOffset = 7;
|
GLint maxProgramTextureGatherOffset = 7;
|
||||||
GLint maxPatchVertices = 32;
|
GLint maxPatchVertices = 32;
|
||||||
GLint maxTessGenLevel = 64;
|
GLint maxTessGenLevel = 64;
|
||||||
|
// Function-scope, and used by every probe group below rather than redeclared inside each
|
||||||
|
// one. Returns whether anything was drained, which is what lets a group tell "the driver
|
||||||
|
// answered" from "the driver rejected the pname and left my local alone".
|
||||||
|
const auto drainErrors = [&glesFuncs]() {
|
||||||
|
Bool hadError = false;
|
||||||
|
if (glesFuncs.glGetError) {
|
||||||
|
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
||||||
|
}
|
||||||
|
return hadError;
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE GENERATOR OF THIS WHOLE BUG FAMILY, closed here. A bare glGetIntegerv/glGetFloatv
|
||||||
|
// of a pname the driver does not have does two damaging things at once: it leaves the
|
||||||
|
// local at whatever the declaration initialised it to - an optimistic number the frontend
|
||||||
|
// then advertises as a capability - and it leaves a GL_INVALID_ENUM in the queue where
|
||||||
|
// the next unrelated probe's caller, or the application's first glGetError, gets blamed
|
||||||
|
// for it. The per-stage storage block, fragment interpolation and buffer texture probes
|
||||||
|
// below already drain and fall back; this unconditional run did neither, which is how
|
||||||
|
// GL_MAX_CLIP_DISTANCES came to be advertised as 8 on a driver with no clip distances at
|
||||||
|
// all. Every pname here that is not ES core is now either gated on the capability that
|
||||||
|
// makes it exist or floored at the value a rejected probe would have left, and the whole
|
||||||
|
// run is bracketed by a drain.
|
||||||
|
drainErrors();
|
||||||
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
|
||||||
|
// GL_SMOOTH_LINE_WIDTH_RANGE / GL_SMOOTH_LINE_WIDTH_GRANULARITY (0x0B22 / 0x0B23) are
|
||||||
|
// desktop-only - ES has never had an antialiased line width query - so on a real GLES
|
||||||
|
// driver these two raise GL_INVALID_ENUM. Kept as probes rather than dropped because the
|
||||||
|
// ANGLE and desktop-GL hosts MobileGL also runs on do answer them; the initialisers are
|
||||||
|
// the GL 4.6 table 23.55 minimum of [1, 1], which is both the honest answer for a driver
|
||||||
|
// that cannot say and what an untouched out-param already holds.
|
||||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
|
||||||
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
|
||||||
glesFuncs.glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, aliasedPointSizeRange);
|
glesFuncs.glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, aliasedPointSizeRange);
|
||||||
glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3DTextureSize);
|
glesFuncs.glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3DTextureSize);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxArrayTextureLayers);
|
glesFuncs.glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxArrayTextureLayers);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &maxCubeMapTextureSize);
|
glesFuncs.glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &maxCubeMapTextureSize);
|
||||||
@@ -1102,8 +1172,25 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
// single test case. 1 is a spec-legal value (the minimum required), so cap
|
// single test case. 1 is a spec-legal value (the minimum required), so cap
|
||||||
// to what is actually implemented instead of forwarding the raw driver limit.
|
// to what is actually implemented instead of forwarding the raw driver limit.
|
||||||
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
|
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
|
||||||
|
// The multisample ceilings above are ES 3.1 state apart from GL_MAX_SAMPLES, which is ES
|
||||||
|
// 3.0, so a 3.0 context rejects five of the six and leaves whatever the out-param held.
|
||||||
|
// One sample is what a rejected probe leaves behind and is also the smallest legal
|
||||||
|
// answer, so clamp rather than trust: a zero reaching GL_Getter would have the frontend
|
||||||
|
// reject the very sample count it just advertised (see GetAdvertisedMaxSamples).
|
||||||
|
maxColorTextureSamples = std::max(maxColorTextureSamples, 1);
|
||||||
|
maxDepthTextureSamples = std::max(maxDepthTextureSamples, 1);
|
||||||
|
maxFramebufferSamples = std::max(maxFramebufferSamples, 1);
|
||||||
|
maxIntegerSamples = std::max(maxIntegerSamples, 1);
|
||||||
|
maxSamples = std::max(maxSamples, 1);
|
||||||
|
maxSampleMaskWords = std::max(maxSampleMaskWords, 1);
|
||||||
|
// ES 3.2 core, or EXT_tessellation_shader on 3.1. Probed rather than version-gated so a
|
||||||
|
// 3.1 driver that HAS the extension still gets to answer; the clamp below is what makes a
|
||||||
|
// rejected query safe, since GL 4.6 table 23.66 and ES 3.2 table 21.45 set the same
|
||||||
|
// minimums the initialisers carry and neither API permits less.
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
glesFuncs.glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
glesFuncs.glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
||||||
|
maxPatchVertices = std::max(maxPatchVertices, 32);
|
||||||
|
maxTessGenLevel = std::max(maxTessGenLevel, 64);
|
||||||
glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
|
glesFuncs.glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET, &minProgramTextureGatherOffset);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
|
glesFuncs.glGetIntegerv(GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET, &maxProgramTextureGatherOffset);
|
||||||
// A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
|
// A driver that leaves the probe untouched (pre-ES 3.1, or an ignored enum) must not
|
||||||
@@ -1140,6 +1227,13 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
||||||
}
|
}
|
||||||
|
// Closes the bracket opened before the run: every local above now holds either the
|
||||||
|
// driver's answer or a floor, and nothing this function asked for is left in the error
|
||||||
|
// queue for a later probe - or the application - to be blamed for.
|
||||||
|
if (drainErrors()) {
|
||||||
|
MGLOG_W("One or more capability queries were rejected by this driver; the affected "
|
||||||
|
"limits keep MobileGL's spec-minimum floors");
|
||||||
|
}
|
||||||
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
|
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
|
||||||
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
|
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
|
||||||
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
|
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
|
||||||
@@ -1152,14 +1246,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
|
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
|
||||||
// where those stages do not exist at all.
|
// where those stages do not exist at all.
|
||||||
{
|
{
|
||||||
const auto drainErrors = [&glesFuncs]() {
|
|
||||||
Bool hadError = false;
|
|
||||||
if (glesFuncs.glGetError) {
|
|
||||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
|
||||||
}
|
|
||||||
return hadError;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Isolate from errors raised by the preceding probes so the drain below reports on
|
// Isolate from errors raised by the preceding probes so the drain below reports on
|
||||||
// these queries only.
|
// these queries only.
|
||||||
drainErrors();
|
drainErrors();
|
||||||
@@ -1195,19 +1281,67 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
}
|
}
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
// GL_MAX_CLIP_DISTANCES is 0x0D32, which ES only ever spells GL_MAX_CLIP_DISTANCES_EXT and
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
// only ever has under GL_EXT_clip_cull_distance. The extension was already resolved into
|
||||||
|
// caps.SupportsClipDistance a few hundred lines above and is the same flag DirectGLES
|
||||||
|
// gates the CLIP_DISTANCEi enable forwarding on, so ask the driver only where the pname
|
||||||
|
// exists; everywhere else the honest 0 stands and no GL_INVALID_ENUM is left behind for an
|
||||||
|
// unrelated query - or the application's first glGetError - to trip over.
|
||||||
|
if (caps.SupportsClipDistance) {
|
||||||
|
drainErrors();
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||||
|
if (drainErrors()) {
|
||||||
|
MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CLIP_DISTANCES was "
|
||||||
|
"rejected; reporting no clip distances");
|
||||||
|
maxClipDistances = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
|
||||||
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
// GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is
|
||||||
|
// what gl_Layer needs). Ask the driver where the pname exists rather than asserting a
|
||||||
|
// convention: it is a statement about which vertex of a primitive supplies gl_Layer, and
|
||||||
|
// MobileGL forwards the geometry stage to the driver rather than implementing the
|
||||||
|
// selection itself, so the driver's answer IS MobileGL's answer. Below ES 3.2 there are
|
||||||
|
// no layered draws to have a convention for and GL_UNDEFINED_VERTEX stands, which GL 4.6
|
||||||
|
// table 23.65 explicitly permits.
|
||||||
|
if (esAtLeast32) {
|
||||||
|
GLint driverLayerConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
|
||||||
|
drainErrors();
|
||||||
|
glesFuncs.glGetIntegerv(GL_LAYER_PROVOKING_VERTEX, &driverLayerConvention);
|
||||||
|
if (!drainErrors()) {
|
||||||
|
layerProvokingVertex = NormalizeProvokingVertexConvention(driverLayerConvention);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GL_MAX_VIEWPORTS (0x825B), GL_VIEWPORT_SUBPIXEL_BITS (0x825C) and GL_VIEWPORT_BOUNDS_RANGE
|
||||||
|
// (0x825D) all arrive with GL_OES_viewport_array and exist nowhere in ES core, so on the
|
||||||
|
// drivers DirectGLES actually runs on all three raise GL_INVALID_ENUM. The values MobileGL
|
||||||
|
// advertises do not change by asking: GL_Getter answers GL_MAX_VIEWPORTS from the frontend
|
||||||
|
// state width (indexed viewport entry points validate against RenderStateParameters::
|
||||||
|
// MAX_VIEWPORTS, so a device answer of 1 would reject indices the state can legitimately
|
||||||
|
// hold), floors GL_SUBPIXEL_BITS at its own 4, and the bounds range is clamped to the core
|
||||||
|
// minimum below. What changes is that the errors stop being manufactured.
|
||||||
|
if (caps.SupportsViewportArray) {
|
||||||
|
GLint driverViewportIndexConvention = static_cast<GLint>(GL_UNDEFINED_VERTEX);
|
||||||
|
drainErrors();
|
||||||
|
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
|
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
|
||||||
|
glesFuncs.glGetIntegerv(GL_VIEWPORT_INDEX_PROVOKING_VERTEX, &driverViewportIndexConvention);
|
||||||
|
if (glesFuncs.glGetFloatv) {
|
||||||
|
glesFuncs.glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, viewportBoundsRange);
|
||||||
|
}
|
||||||
|
if (drainErrors()) {
|
||||||
|
MGLOG_W("GL_OES_viewport_array is advertised but its viewport limit queries were "
|
||||||
|
"rejected; keeping the OpenGL core minimums");
|
||||||
|
maxViewports = 16;
|
||||||
|
viewportSubpixelBits = 0;
|
||||||
|
viewportBoundsRange[0] = -32768.0f;
|
||||||
|
viewportBoundsRange[1] = 32767.0f;
|
||||||
|
} else {
|
||||||
|
viewportIndexProvokingVertex =
|
||||||
|
NormalizeProvokingVertexConvention(driverViewportIndexConvention);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
|
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
|
||||||
const auto drainErrors = [&glesFuncs]() {
|
|
||||||
Bool hadError = false;
|
|
||||||
if (glesFuncs.glGetError) {
|
|
||||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
|
||||||
}
|
|
||||||
return hadError;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Isolate these optional queries from errors raised by preceding capability
|
// Isolate these optional queries from errors raised by preceding capability
|
||||||
// probes, then consume any query error so initialization never leaks it into
|
// probes, then consume any query error so initialization never leaks it into
|
||||||
// the application's first glGetError call.
|
// the application's first glGetError call.
|
||||||
@@ -1368,8 +1502,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
||||||
caps.MaxDrawBuffers = maxDrawBuffers;
|
caps.MaxDrawBuffers = maxDrawBuffers;
|
||||||
caps.MaxColorAttachments = maxColorAttachments;
|
caps.MaxColorAttachments = maxColorAttachments;
|
||||||
caps.MaxClipDistances = maxClipDistances;
|
// 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.MaxViewports = maxViewports;
|
caps.MaxViewports = maxViewports;
|
||||||
|
caps.LayerProvokingVertex = layerProvokingVertex;
|
||||||
|
caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex;
|
||||||
caps.MaxViewportWidth = maxViewportDims[0];
|
caps.MaxViewportWidth = maxViewportDims[0];
|
||||||
caps.MaxViewportHeight = maxViewportDims[1];
|
caps.MaxViewportHeight = maxViewportDims[1];
|
||||||
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
||||||
@@ -1450,12 +1588,20 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
|
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
|
||||||
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
||||||
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
||||||
MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d", caps.MaxClipDistances);
|
// Worth spelling the reason out for the same reason the per-stage storage block counts
|
||||||
|
// are: a zero here is what stops an application's gl_ClipDistance from ever clipping, and
|
||||||
|
// reading it back from an artifact is the difference between "MobileGL dropped my draw"
|
||||||
|
// 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_VIEWPORTS: %d", caps.MaxViewports);
|
MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports);
|
||||||
MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
|
MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight);
|
||||||
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
|
MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin,
|
||||||
caps.ViewportBoundsRangeMax);
|
caps.ViewportBoundsRangeMax);
|
||||||
MGLOG_I(" GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
|
MGLOG_I(" GL_VIEWPORT_SUBPIXEL_BITS: %d", caps.ViewportSubpixelBits);
|
||||||
|
MGLOG_I(" GL_LAYER_PROVOKING_VERTEX: %s", ProvokingVertexConventionName(caps.LayerProvokingVertex));
|
||||||
|
MGLOG_I(" GL_VIEWPORT_INDEX_PROVOKING_VERTEX: %s",
|
||||||
|
ProvokingVertexConventionName(caps.ViewportIndexProvokingVertex));
|
||||||
|
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance =
|
caps.IndirectDrawInstanceIdIncludesBaseInstance =
|
||||||
ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
|
ProbeIndirectInstanceIdIncludesBaseInstance(caps, glesFuncs);
|
||||||
@@ -1479,6 +1625,14 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
|
caps.AvoidSamplerMipmapMinFilter ? "true" : "false");
|
||||||
MGLOG_I(" Avoid explicit LOD bias: %s", caps.AvoidExplicitLodBias ? "true" : "false");
|
MGLOG_I(" Avoid explicit LOD bias: %s", caps.AvoidExplicitLodBias ? "true" : "false");
|
||||||
|
|
||||||
|
// Last line of defence. Capability init is the very first thing that touches the driver,
|
||||||
|
// so anything it leaves in the error queue surfaces at the APPLICATION's first
|
||||||
|
// glGetError and gets attributed to whatever call the app happened to make. Every group
|
||||||
|
// above drains its own, but a probe added later must not be able to reintroduce the leak.
|
||||||
|
if (drainErrors()) {
|
||||||
|
MGLOG_W("Capability initialization left a GL error behind; it has been consumed so it "
|
||||||
|
"cannot surface at the application's first glGetError");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Util::BackendLoader
|
} // namespace MobileGL::MG_Util::BackendLoader
|
||||||
|
|||||||
@@ -1275,8 +1275,16 @@ namespace MobileGL {
|
|||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
Int MaxClipDistances = 8;
|
// Zero is a legal answer, not a placeholder: ES reaches clip distances only through
|
||||||
|
// GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe
|
||||||
|
// in FillInGLESCapabilities.
|
||||||
|
Int MaxClipDistances = 0;
|
||||||
Int MaxViewports = 16;
|
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
|
||||||
|
// a driver that has neither is honestly saying.
|
||||||
|
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
|
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||||
Int MaxViewportWidth = 16384;
|
Int MaxViewportWidth = 16384;
|
||||||
Int MaxViewportHeight = 16384;
|
Int MaxViewportHeight = 16384;
|
||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
|
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
|
||||||
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
|
||||||
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
|
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
|
||||||
|
caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE;
|
||||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
|
||||||
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
||||||
HasUsableShaderSubgroupSupport(subgroupProps);
|
HasUsableShaderSubgroupSupport(subgroupProps);
|
||||||
@@ -331,6 +332,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
caps.SupportsVertexPipelineStoresAndAtomics = false;
|
||||||
caps.SupportsFragmentStoresAndAtomics = false;
|
caps.SupportsFragmentStoresAndAtomics = false;
|
||||||
caps.SupportsGeometryShader = false;
|
caps.SupportsGeometryShader = false;
|
||||||
|
caps.SupportsShaderClipDistance = false;
|
||||||
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
|
||||||
caps.SupportsShaderSubgroup = false;
|
caps.SupportsShaderSubgroup = false;
|
||||||
caps.SubgroupSize = 0;
|
caps.SubgroupSize = 0;
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ namespace MobileGL {
|
|||||||
Bool SupportsVertexPipelineStoresAndAtomics = false;
|
Bool SupportsVertexPipelineStoresAndAtomics = false;
|
||||||
Bool SupportsFragmentStoresAndAtomics = false;
|
Bool SupportsFragmentStoresAndAtomics = false;
|
||||||
Bool SupportsGeometryShader = false;
|
Bool SupportsGeometryShader = false;
|
||||||
|
// VkPhysicalDeviceFeatures::shaderClipDistance. maxClipDistances is a LIMIT and is
|
||||||
|
// reported whatever the feature says, so the limit alone does not mean a module may
|
||||||
|
// declare ClipDistance - VulkanRenderer enables the feature only where the physical
|
||||||
|
// 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;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Bool SupportsShaderSubgroup = false;
|
Bool SupportsShaderSubgroup = false;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
|
|||||||
@@ -521,9 +521,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Warn("64-bit vertex attributes",
|
builder.Warn("64-bit vertex attributes",
|
||||||
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
|
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
|
||||||
"above there is no 64-bit shader input left to feed either); "
|
"above there is no 64-bit shader input left to feed either); "
|
||||||
"glVertexAttribLFormat / glVertexArrayAttribLFormat report "
|
"glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is "
|
||||||
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), "
|
"queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute "
|
||||||
"which a demoted dvec input reads correctly");
|
"reads its generic current value - feed the attribute with "
|
||||||
|
"glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads "
|
||||||
|
"correctly");
|
||||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||||
builder.Pass("Tessellation patch parameters",
|
builder.Pass("Tessellation patch parameters",
|
||||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||||
@@ -556,8 +558,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
} else {
|
} else {
|
||||||
builder.Warn("GL_EXT_render_snorm",
|
builder.Warn("GL_EXT_render_snorm",
|
||||||
"not supported; signed-normalized formats are texture-only, so every SNORM "
|
"not supported; signed-normalized formats are texture-only, so every SNORM "
|
||||||
"render target is stored as a float (GL_RGBA8_SNORM/GL_RGB8_SNORM -> "
|
"render target is stored as a float (8-bit -> *16F, 16-bit -> *32F, which "
|
||||||
"GL_RGBA16F) and its fragment outputs are clamped to [-1,1] in software");
|
"is the narrowest float that still holds a 16-bit SNORM channel exactly) "
|
||||||
|
"and its fragment outputs are clamped to [-1,1] in software");
|
||||||
}
|
}
|
||||||
// FAIL, not WARN: ES 3.x core makes every float format texture-only, and every Iris
|
// FAIL, not WARN: ES 3.x core makes every float format texture-only, and every Iris
|
||||||
// shaderpack renders into at least GL_R11F_G11F_B10F (Complementary's colortex0, BSL's
|
// shaderpack renders into at least GL_R11F_G11F_B10F (Complementary's colortex0, BSL's
|
||||||
@@ -2336,8 +2339,10 @@ namespace MobileGL::MG_Util::SelfTest {
|
|||||||
builder.Warn("64-bit vertex attributes",
|
builder.Warn("64-bit vertex attributes",
|
||||||
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
|
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
|
||||||
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
|
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
|
||||||
"anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with "
|
"anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED "
|
||||||
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly");
|
"64-bit array is DROPPED at pipeline build and the attribute reads its generic "
|
||||||
|
"current value - feed the attribute with glVertexAttribPointer(GL_FLOAT) instead, "
|
||||||
|
"which a demoted dvec input reads correctly");
|
||||||
|
|
||||||
Bool shaderDrawParameters = false;
|
Bool shaderDrawParameters = false;
|
||||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
HashValue(state, env.maxComputeWorkGroupSize[0]);
|
HashValue(state, env.maxComputeWorkGroupSize[0]);
|
||||||
HashValue(state, env.maxComputeWorkGroupSize[1]);
|
HashValue(state, env.maxComputeWorkGroupSize[1]);
|
||||||
HashValue(state, env.maxComputeWorkGroupSize[2]);
|
HashValue(state, env.maxComputeWorkGroupSize[2]);
|
||||||
|
HashValue(state, env.maxComputeWorkGroupCount[0]);
|
||||||
|
HashValue(state, env.maxComputeWorkGroupCount[1]);
|
||||||
|
HashValue(state, env.maxComputeWorkGroupCount[2]);
|
||||||
HashValue(state, env.maxComputeWorkGroupInvocations);
|
HashValue(state, env.maxComputeWorkGroupInvocations);
|
||||||
HashValue(state, env.backend);
|
HashValue(state, env.backend);
|
||||||
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
|
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
|
||||||
@@ -41,6 +44,51 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint64 ComputeFrontendCompileEnvFingerprint(const CompileEnv& env) {
|
||||||
|
Uint64 state = 0xff51afd7ed558ccdull;
|
||||||
|
// The DynamicBackendParameters limits BuildTBuiltInResource copies into
|
||||||
|
// TBuiltInResource. Enumerated ONE BY ONE rather than hashed as a struct,
|
||||||
|
// deliberately: hashing all of DynamicBackendParameters would drag ~50 backend-only
|
||||||
|
// limits into a key that is supposed to be backend-agnostic, and every one of them
|
||||||
|
// would be a false miss. Keep this list in step with BuildTBuiltInResource.
|
||||||
|
HashValue(state, env.params.MaxImageUnits);
|
||||||
|
HashValue(state, env.params.MaxDrawBuffers);
|
||||||
|
HashValue(state, env.params.MaxVertexImageUniforms);
|
||||||
|
HashValue(state, env.params.MaxGeometryImageUniforms);
|
||||||
|
HashValue(state, env.params.MaxFragmentImageUniforms);
|
||||||
|
HashValue(state, env.params.MaxComputeImageUniforms);
|
||||||
|
HashValue(state, env.params.MaxCombinedImageUniforms);
|
||||||
|
// Added when wave3 (cb155c5b) made this one env-derived. It expands into the
|
||||||
|
// gl_MaxComputeTextureImageUnits built-in constant, so a compute module that reads
|
||||||
|
// that constant generates DIFFERENT SPIR-V under two backends that disagree on it.
|
||||||
|
HashValue(state, env.params.MaxComputeTextureImageUnits);
|
||||||
|
// Added when wave4 (4fc3531d) made this one env-derived, and the same class again:
|
||||||
|
// glslang REJECTS gl_ClipDistance[i] for i >= maxClipDistances at parse (ParseHelper)
|
||||||
|
// 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 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 -
|
||||||
|
// that day is this merge. glslang expands BOTH of them into built-in constants
|
||||||
|
// (Initialize.cpp: "const ivec3 gl_MaxComputeWorkGroupCount = ivec3(%d,%d,%d)" and the
|
||||||
|
// same for gl_MaxComputeWorkGroupSize), so this is an INDEPENDENCE break, not merely a
|
||||||
|
// reachability one: a compute shader that reads gl_MaxComputeWorkGroupSize compiles to
|
||||||
|
// materially different SPIR-V on a driver reporting z=64 than on one reporting z=1024.
|
||||||
|
for (Uint index = 0; index < 3; ++index) {
|
||||||
|
HashValue(state, env.maxComputeWorkGroupSize[index]);
|
||||||
|
HashValue(state, env.maxComputeWorkGroupCount[index]);
|
||||||
|
}
|
||||||
|
// The two inputs to GetReflectionVertexAttribLimit. Hashed as inputs rather than as
|
||||||
|
// the resolved limit so this stays in one translation unit; that is coarser (two
|
||||||
|
// envs whose MaxVertexAttribs both exceed the storage capacity resolve to the same
|
||||||
|
// limit yet hash differently) but coarser means a false MISS, never a false hit.
|
||||||
|
HashValue(state, env.params.MaxVertexAttribs);
|
||||||
|
const Uint8 hasBackend = env.HasBackend() ? 1u : 0u;
|
||||||
|
HashValue(state, hasBackend);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
SharedPtr<const CompileEnv> CaptureCompileEnv() {
|
SharedPtr<const CompileEnv> CaptureCompileEnv() {
|
||||||
auto env = MakeShared<CompileEnv>();
|
auto env = MakeShared<CompileEnv>();
|
||||||
|
|
||||||
@@ -51,19 +99,23 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
|
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must
|
// GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they
|
||||||
// happen here, on the context thread, and exactly once per context. The frontend
|
// must happen here, on the context thread, and exactly once per context. The frontend
|
||||||
// minimum is the floor, matching what GL_Getter reports.
|
// minimum is the floor, matching what GL_Getter reports - both sides now floor at the
|
||||||
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
|
// shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them.
|
||||||
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
|
|
||||||
for (Uint index = 0; index < 3; ++index) {
|
for (Uint index = 0; index < 3; ++index) {
|
||||||
Int backendValue = 0;
|
Int backendSize = 0;
|
||||||
|
Int backendCount = 0;
|
||||||
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
|
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
|
||||||
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
|
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
|
||||||
&backendValue);
|
&backendSize);
|
||||||
|
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index,
|
||||||
|
&backendCount);
|
||||||
}
|
}
|
||||||
env->maxComputeWorkGroupSize[index] =
|
env->maxComputeWorkGroupSize[index] =
|
||||||
std::max(static_cast<Uint>(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]);
|
std::max(static_cast<Uint>(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]);
|
||||||
|
env->maxComputeWorkGroupCount[index] =
|
||||||
|
std::max(static_cast<Uint>(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
|
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
|
||||||
@@ -73,6 +125,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
: kFrontendMaxComputeWorkGroupInvocations;
|
: kFrontendMaxComputeWorkGroupInvocations;
|
||||||
|
|
||||||
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
||||||
|
env->frontendFingerprint = ComputeFrontendCompileEnvFingerprint(*env);
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +135,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
static const SharedPtr<const CompileEnv> kDefault = [] {
|
static const SharedPtr<const CompileEnv> kDefault = [] {
|
||||||
auto env = MakeShared<CompileEnv>();
|
auto env = MakeShared<CompileEnv>();
|
||||||
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
env->fingerprint = ComputeCompileEnvFingerprint(*env);
|
||||||
|
env->frontendFingerprint = ComputeFrontendCompileEnvFingerprint(*env);
|
||||||
return SharedPtr<const CompileEnv>(Move(env));
|
return SharedPtr<const CompileEnv>(Move(env));
|
||||||
}();
|
}();
|
||||||
return kDefault;
|
return kDefault;
|
||||||
|
|||||||
@@ -12,6 +12,18 @@
|
|||||||
#include <MG_Backend/BackendObject.h>
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||||
|
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE
|
||||||
|
// place because three separate readers have to agree on them: CaptureCompileEnv (which floors
|
||||||
|
// the backend's answer at them), GL_Getter (which answers the same query the same way) and
|
||||||
|
// BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against
|
||||||
|
// the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies,
|
||||||
|
// and the z one disagreed: glslang compiled against 1024 while the context advertised 64.
|
||||||
|
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535};
|
||||||
|
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64};
|
||||||
|
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number
|
||||||
|
// glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to.
|
||||||
|
inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024;
|
||||||
|
|
||||||
// everything outside (stage, source) this reads - advertised extensions and backend limits -
|
// everything outside (stage, source) this reads - advertised extensions and backend limits -
|
||||||
// so the transformation is a pure function of its three arguments and can run on a worker
|
// so the transformation is a pure function of its three arguments and can run on a worker
|
||||||
// thread.
|
// thread.
|
||||||
@@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
struct CompileEnv {
|
struct CompileEnv {
|
||||||
// --- compute limits: the ONLY former real-driver read in the pipeline ---
|
// --- compute limits: the ONLY former real-driver read in the pipeline ---
|
||||||
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
|
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
|
||||||
Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1],
|
||||||
|
MIN_COMPUTE_WORK_GROUP_SIZE[2]};
|
||||||
|
// GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is:
|
||||||
|
// gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline
|
||||||
|
// needs the number the context advertises without reaching back to the live backend.
|
||||||
|
Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1],
|
||||||
|
MIN_COMPUTE_WORK_GROUP_COUNT[2]};
|
||||||
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
|
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
|
||||||
Uint64 maxComputeWorkGroupInvocations = 1024;
|
Uint64 maxComputeWorkGroupInvocations = 1024;
|
||||||
|
|
||||||
@@ -48,6 +66,74 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
|
|
||||||
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
|
Uint64 fingerprint = 0; // set by CaptureCompileEnv()
|
||||||
|
|
||||||
|
// The FRONT-END half of the environment: the subset of the fields above that can
|
||||||
|
// change what glslang PRODUCES - the SPIR-V or the reflection - as opposed to what a
|
||||||
|
// BACKEND later does with the result. This, and never `fingerprint`, is what the L1
|
||||||
|
// shader translation memo keys on, because L1 is backend-agnostic BY CONTRACT: two
|
||||||
|
// contexts on different GPUs compiling the same GLSL must share one L1 entry.
|
||||||
|
//
|
||||||
|
// THE LINE THIS DRAWS. "Backend-agnostic" means BACKEND IDENTITY is out - the vendor,
|
||||||
|
// the extension list, which of DirectGLES/DirectVulkan is active, every capability bit
|
||||||
|
// that merely steers the transpile. It does NOT mean backend-DERIVED VALUES are out: a
|
||||||
|
// resource limit that glslang enforces at parse, or expands into a built-in constant,
|
||||||
|
// is a front-end INPUT no matter where the number came from, and dropping it would be
|
||||||
|
// a silent miscompile rather than a backend leak. A driver with 16 vertex attribs and
|
||||||
|
// one with 32 genuinely reflect the same GLSL differently.
|
||||||
|
//
|
||||||
|
// WHAT IS IN IT (audited; re-audit whenever a new env read appears in the front end):
|
||||||
|
// * the DynamicBackendParameters fields BuildTBuiltInResource copies into
|
||||||
|
// TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms,
|
||||||
|
// MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms,
|
||||||
|
// MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. 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.
|
||||||
|
// * 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
|
||||||
|
// cap - exactly the migration the old exclusion note said would force them in
|
||||||
|
// here. They are not merely a reject gate: glslang expands both into built-in
|
||||||
|
// CONSTANTS (gl_MaxComputeWorkGroupSize, gl_MaxComputeWorkGroupCount), so a
|
||||||
|
// compute module that reads one generates different SPIR-V under two drivers that
|
||||||
|
// report different numbers.
|
||||||
|
// * MaxVertexAttribs and the HasBackend() bit: the two inputs to ProgramLinkTask's
|
||||||
|
// GetReflectionVertexAttribLimit, which bounds how many vertex input locations
|
||||||
|
// reflection records - so they change the REFLECTION the memo carries.
|
||||||
|
//
|
||||||
|
// The sharding this costs is nil in practice and worth naming so nobody re-litigates
|
||||||
|
// it: a process has ONE active backend at a time and CompileEnv is re-captured when
|
||||||
|
// that changes, so no live run ever has two of these fingerprints competing for the
|
||||||
|
// same L1 entries. The cost would only appear on a future cross-device DISK tier,
|
||||||
|
// where it is the correct cost - those devices really do compile that GLSL differently.
|
||||||
|
//
|
||||||
|
// WHAT IS DELIBERATELY OUT:
|
||||||
|
// * `backend` beyond the HasBackend() bit. Nothing in the parse, the link or
|
||||||
|
// GlslangToSpv branches on which backend is active - ShaderAttrib::flags is 0 on
|
||||||
|
// both production parse paths (ShaderCompileTask::RunCompilePipeline and
|
||||||
|
// ClaimParsedShader). Backend identity steers the TRANSPILE, which is L2's key.
|
||||||
|
// * `advertisedExtensions`. Its only front-end consumer is ShaderSourceProcessor's
|
||||||
|
// FilterUnsupportedGpuShaderInt64, which REWRITES THE SOURCE TEXT - and the
|
||||||
|
// preprocessed text is in the L1 key verbatim, a strictly finer discriminator
|
||||||
|
// than the extension list. (E_GL_ARB_gpu_shader_fp64 is never read by the front
|
||||||
|
// end at all: MOBILEGL_ADVERTISE_FP64 only adds it to the extension STRING the
|
||||||
|
// application queries, and DemoteFloat64Pass runs unconditionally either way, so
|
||||||
|
// fp64 GLSL translates identically with the flag on or off.)
|
||||||
|
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
|
||||||
|
// the backends, never by the parse, the link or reflection.
|
||||||
|
// * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
|
||||||
|
// moved into the list above at the wave3 merge. glslang has no
|
||||||
|
// gl_MaxComputeWorkGroupInvocations built-in and BuildTBuiltInResource does not
|
||||||
|
// read this field, so its sole consumer is still ValidateComputeLocalSizeLimits, a
|
||||||
|
// pre-parse ACCEPT/REJECT gate. A rejected shader fails its compile, so its
|
||||||
|
// program never reaches the tail of the link and no L1 entry is ever created under
|
||||||
|
// a rejecting environment; an accepted one parses identically at any value.
|
||||||
|
// THIS ONE IS A REACHABILITY ARGUMENT, NOT AN INDEPENDENCE ONE, and it is now the
|
||||||
|
// only such argument left in this classification. The moment anything hands this
|
||||||
|
// value to glslang - a TBuiltInResource field, a built-in constant - it MUST move
|
||||||
|
// into the fingerprint, exactly as its companions just did.
|
||||||
|
Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
|
||||||
|
|
||||||
Bool HasBackend() const { return backend != BackendType::Unknown; }
|
Bool HasBackend() const { return backend != BackendType::Unknown; }
|
||||||
// Matches the historical rule exactly: with no active backend every extension counts
|
// Matches the historical rule exactly: with no active backend every extension counts
|
||||||
// as advertised, because the frontend then has nothing to gate against.
|
// as advertised, because the frontend then has nothing to gate against.
|
||||||
@@ -62,6 +148,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// different envs really do produce different P0b cache keys.
|
// different envs really do produce different P0b cache keys.
|
||||||
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
|
Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env);
|
||||||
|
|
||||||
|
// The backend-agnostic half; see CompileEnv::frontendFingerprint for the classification
|
||||||
|
// and the evidence behind each call. Public so a test can assert both directions: that a
|
||||||
|
// backend-only difference produces the SAME value (which is what pins L1's
|
||||||
|
// backend-agnosticism) and that a front-end limit produces a different one.
|
||||||
|
Uint64 ComputeFrontendCompileEnvFingerprint(const CompileEnv& env);
|
||||||
|
|
||||||
// GL thread only: this is where the GL_MAX_COMPUTE_WORK_GROUP_SIZE queries live now.
|
// GL thread only: this is where the GL_MAX_COMPUTE_WORK_GROUP_SIZE queries live now.
|
||||||
SharedPtr<const CompileEnv> CaptureCompileEnv();
|
SharedPtr<const CompileEnv> CaptureCompileEnv();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "SpirvPasses/LowerViewportIndexPass.h"
|
#include "SpirvPasses/LowerViewportIndexPass.h"
|
||||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||||
|
#include "SpirvPasses/UniquifyIoBlockNamesPass.h"
|
||||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||||
@@ -82,19 +83,11 @@ namespace MobileGL {
|
|||||||
Resources.maxFragmentInputVectors = 15;
|
Resources.maxFragmentInputVectors = 15;
|
||||||
Resources.minProgramTexelOffset = -8;
|
Resources.minProgramTexelOffset = -8;
|
||||||
Resources.maxProgramTexelOffset = 7;
|
Resources.maxProgramTexelOffset = 7;
|
||||||
Resources.maxClipDistances = 8;
|
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
|
||||||
Resources.maxComputeWorkGroupCountX = 65535;
|
|
||||||
Resources.maxComputeWorkGroupCountY = 65535;
|
|
||||||
Resources.maxComputeWorkGroupCountZ = 65535;
|
|
||||||
Resources.maxComputeWorkGroupSizeX = 1024;
|
|
||||||
Resources.maxComputeWorkGroupSizeY = 1024;
|
|
||||||
// TODO: Drive glslang compute resource limits from the active backend instead of this permissive cap.
|
|
||||||
Resources.maxComputeWorkGroupSizeZ = 1024;
|
|
||||||
Resources.maxComputeUniformComponents = 1024;
|
|
||||||
Resources.maxComputeTextureImageUnits = 16;
|
Resources.maxComputeTextureImageUnits = 16;
|
||||||
Resources.maxComputeImageUniforms = 8;
|
Resources.maxComputeImageUniforms = 8;
|
||||||
Resources.maxComputeAtomicCounters = 8;
|
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||||
Resources.maxComputeAtomicCounterBuffers = 1;
|
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||||
Resources.maxVaryingComponents = 60;
|
Resources.maxVaryingComponents = 60;
|
||||||
Resources.maxVertexOutputComponents = 64;
|
Resources.maxVertexOutputComponents = 64;
|
||||||
Resources.maxGeometryInputComponents = 64;
|
Resources.maxGeometryInputComponents = 64;
|
||||||
@@ -132,16 +125,22 @@ namespace MobileGL {
|
|||||||
Resources.maxTessControlAtomicCounters = 0;
|
Resources.maxTessControlAtomicCounters = 0;
|
||||||
Resources.maxTessEvaluationAtomicCounters = 0;
|
Resources.maxTessEvaluationAtomicCounters = 0;
|
||||||
Resources.maxGeometryAtomicCounters = 0;
|
Resources.maxGeometryAtomicCounters = 0;
|
||||||
Resources.maxFragmentAtomicCounters = 8;
|
Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||||
Resources.maxCombinedAtomicCounters = 8;
|
Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
|
||||||
Resources.maxAtomicCounterBindings = 1;
|
// Every atomic-counter limit below is the one glGetIntegerv answers; the shared
|
||||||
|
// constants in Types.h are what keeps the two sides from drifting apart again.
|
||||||
|
// gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these
|
||||||
|
// (Initialize.cpp), and the binding count is also the ceiling glslang checks a
|
||||||
|
// `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter
|
||||||
|
// outside binding 0 failed to compile.
|
||||||
|
Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
|
||||||
Resources.maxVertexAtomicCounterBuffers = 0;
|
Resources.maxVertexAtomicCounterBuffers = 0;
|
||||||
Resources.maxTessControlAtomicCounterBuffers = 0;
|
Resources.maxTessControlAtomicCounterBuffers = 0;
|
||||||
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
|
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
|
||||||
Resources.maxGeometryAtomicCounterBuffers = 0;
|
Resources.maxGeometryAtomicCounterBuffers = 0;
|
||||||
Resources.maxFragmentAtomicCounterBuffers = 1;
|
Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||||
Resources.maxCombinedAtomicCounterBuffers = 1;
|
Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
|
||||||
Resources.maxAtomicCounterBufferSize = 16384;
|
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
|
||||||
Resources.maxTransformFeedbackBuffers = 4;
|
Resources.maxTransformFeedbackBuffers = 4;
|
||||||
Resources.maxTransformFeedbackInterleavedComponents = 64;
|
Resources.maxTransformFeedbackInterleavedComponents = 64;
|
||||||
Resources.maxCullDistances = 8;
|
Resources.maxCullDistances = 8;
|
||||||
@@ -160,6 +159,14 @@ namespace MobileGL {
|
|||||||
// Resource checking must describe the same backend contract exposed through
|
// Resource checking must describe the same backend contract exposed through
|
||||||
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
|
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
|
||||||
// TBuiltInResource when Iris compiles shaders concurrently.
|
// TBuiltInResource when Iris compiles shaders concurrently.
|
||||||
|
//
|
||||||
|
// MEMO-HAZARD RULE FOR THIS BLOCK. Everything below is an env-derived value that
|
||||||
|
// glslang enforces at parse AND expands into a built-in constant, so every one of
|
||||||
|
// them can change the SPIR-V a module generates. EVERY LINE BELOW MUST BE HASHED
|
||||||
|
// BY ComputeFrontendCompileEnvFingerprint(), which is the L1 shader-translation
|
||||||
|
// memo's environment key - adding a read here without adding it there is a silent
|
||||||
|
// miscompile, not a slow path. See the classification on
|
||||||
|
// CompileEnv::frontendFingerprint.
|
||||||
const MG_Backend::DynamicBackendParameters fallbackParameters{};
|
const MG_Backend::DynamicBackendParameters fallbackParameters{};
|
||||||
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
const auto& activeBackend = MG_Backend::pActiveBackendObject;
|
||||||
const auto& dynamicParameters =
|
const auto& dynamicParameters =
|
||||||
@@ -173,6 +180,33 @@ namespace MobileGL {
|
|||||||
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
|
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
|
||||||
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
|
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
|
||||||
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
|
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
|
||||||
|
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
|
||||||
|
// 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,
|
||||||
|
// the backend's shader compile fails somewhere the frontend never surfaces, and
|
||||||
|
// the draw renders nothing" into an honest glCompileShader error with a log. It is
|
||||||
|
// 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 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
|
||||||
|
// CaptureCompileEnv already issued once on the GL thread and floored at the core
|
||||||
|
// minimum exactly as GL_Getter does. Reading the same snapshot here is what makes
|
||||||
|
// gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction
|
||||||
|
// (KHR-GL43.compute_shader.max compares them); the z component was 1024 here
|
||||||
|
// against the 64 every ES driver reports. A null env is the standalone/test entry
|
||||||
|
// point, which has no context to have queried one - the core minimums stand, which
|
||||||
|
// is what a default-constructed CompileEnv carries anyway.
|
||||||
|
const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE;
|
||||||
|
const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT;
|
||||||
|
Resources.maxComputeWorkGroupSizeX = static_cast<int>(maxWorkGroupSize[0]);
|
||||||
|
Resources.maxComputeWorkGroupSizeY = static_cast<int>(maxWorkGroupSize[1]);
|
||||||
|
Resources.maxComputeWorkGroupSizeZ = static_cast<int>(maxWorkGroupSize[2]);
|
||||||
|
Resources.maxComputeWorkGroupCountX = static_cast<int>(maxWorkGroupCount[0]);
|
||||||
|
Resources.maxComputeWorkGroupCountY = static_cast<int>(maxWorkGroupCount[1]);
|
||||||
|
Resources.maxComputeWorkGroupCountZ = static_cast<int>(maxWorkGroupCount[2]);
|
||||||
|
|
||||||
Resources.limits.nonInductiveForLoops = true;
|
Resources.limits.nonInductiveForLoops = true;
|
||||||
Resources.limits.whileLoops = true;
|
Resources.limits.whileLoops = true;
|
||||||
@@ -756,6 +790,42 @@ namespace MobileGL {
|
|||||||
outName);
|
outName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ShaderCompiler::ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
|
||||||
|
std::set<String>& collidingBlockNames,
|
||||||
|
std::set<String>& declaredNames) {
|
||||||
|
if (binary.empty()) {
|
||||||
|
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||||
|
// no SPIR-V has no block names to report, and parsing it would push a
|
||||||
|
// spurious diagnostic through the message consumer.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||||
|
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ProbeIoBlockNamesForEssl"), binary.data(),
|
||||||
|
binary.size());
|
||||||
|
if (!context) {
|
||||||
|
// Unparseable here means unusable downstream too; let the ordinary transpile
|
||||||
|
// path produce the error rather than inventing a rename plan from it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniquifyIoBlockNamesPass::ProbeIoBlockNames(context.get(), collidingBlockNames, declaredNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderCompiler::UniquifyIoBlockNamesForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
std::set<String>& renamedBlockNames,
|
||||||
|
Vector<uint32_t>& outputBinary,
|
||||||
|
const bool enableSpirvValidation) {
|
||||||
|
using namespace spvtools;
|
||||||
|
if (inputBlockRenames.empty() && outputBlockRenames.empty()) return false;
|
||||||
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(UniquifyIoBlockNamesPass::CreateUniquifyIoBlockNamesPass(
|
||||||
|
inputBlockRenames, outputBlockRenames, &renamedBlockNames));
|
||||||
|
|
||||||
|
return RunOptimizerChecked("UniquifyIoBlockNamesForEssl", optimizer, inputBinary,
|
||||||
|
outputBinary, true, enableSpirvValidation);
|
||||||
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
const bool enableSpirvValidation) {
|
const bool enableSpirvValidation) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "glslang/TVarEntryInfo.h"
|
#include "glslang/TVarEntryInfo.h"
|
||||||
#include "glslang/TMglGlslIoResolver.h"
|
#include "glslang/TMglGlslIoResolver.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
@@ -101,6 +102,29 @@ namespace MobileGL {
|
|||||||
static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName,
|
static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName,
|
||||||
const std::set<String>& flattenedBlockNames,
|
const std::set<String>& flattenedBlockNames,
|
||||||
String& outName);
|
String& outName);
|
||||||
|
// Adds to `collidingBlockNames` every inter-stage interface block this stage
|
||||||
|
// declares in BOTH directions at once (`in FOO {...}; out FOO {...}`, which
|
||||||
|
// desktop GLSL allows because its input and output block namespaces are
|
||||||
|
// separate), and to `declaredNames` every name the module spells. The gate for
|
||||||
|
// UniquifyIoBlockNamesForEssl below, and the source of the name set a
|
||||||
|
// replacement has to avoid. Reads the module; never rewrites it.
|
||||||
|
static void ProbeIoBlockNamesForEssl(const Vector<Uint32>& binary,
|
||||||
|
std::set<String>& collidingBlockNames,
|
||||||
|
std::set<String>& declaredNames);
|
||||||
|
// Renames inter-stage interface BLOCK types so the collision the probe above
|
||||||
|
// found gets one spelling per producing stage. `inputBlockRenames` applies to
|
||||||
|
// blocks this stage consumes and `outputBlockRenames` to blocks it produces,
|
||||||
|
// both planned program-wide by the caller so a producer and its consumer keep
|
||||||
|
// matching; `renamedBlockNames` reports the original names this stage actually
|
||||||
|
// rewrote. SPIRV-Cross re-emits two same-named blocks verbatim and the Mali ES
|
||||||
|
// driver then loses the output block's payload. Only for the DirectGLES
|
||||||
|
// transpile path. See UniquifyIoBlockNamesPass.
|
||||||
|
static bool UniquifyIoBlockNamesForEssl(const Vector<Uint32>& inputBinary,
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
std::set<String>& renamedBlockNames,
|
||||||
|
Vector<uint32_t>& outputBinary,
|
||||||
|
bool enableSpirvValidation = false);
|
||||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
@@ -818,6 +819,116 @@ namespace {
|
|||||||
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GLSL 4.30 4.1.9 allows an interface-block member array to be left unsized when it is NOT the
|
||||||
|
// last member; it is then implicitly sized by the largest constant index the shader uses.
|
||||||
|
// glslang implements the SIZING - adoptImplicitArraySizes, at link - but computes the block's
|
||||||
|
// member OFFSETS at DECLARATION time (fixBlockUniformOffsets), where the array is still
|
||||||
|
// unsized and so contributes zero bytes. Every member after it is therefore laid out on top of
|
||||||
|
// it: `vec4 a[]; vec4 b;` puts BOTH at offset 0, and a shader reading `b` gets `a[0]`
|
||||||
|
// (KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6, whose degenerate triangle
|
||||||
|
// rasterizes nothing at all).
|
||||||
|
//
|
||||||
|
// The source level is the only place the two can be reconciled, because the offset pass runs
|
||||||
|
// before a single statement has been parsed. Deliberately narrow: it fires only on a `buffer`
|
||||||
|
// block (no other block kind may hold an unsized member at all), only on a member that is not
|
||||||
|
// the last one, and only when every subscript of that member's name in the source is a decimal
|
||||||
|
// literal. Anything outside that shape is left exactly as it was - and the shape itself has no
|
||||||
|
// correct behaviour today, so the rewrite cannot take a working case away.
|
||||||
|
void SizeNonFinalUnsizedBufferBlockMembers(MobileGL::String& source) {
|
||||||
|
// Both tokens must be present for the shape to exist, and "[]" is absent from essentially
|
||||||
|
// every real shader source, so this is the whole cost for them.
|
||||||
|
if (source.find("[]") == MobileGL::String::npos || source.find("buffer") == MobileGL::String::npos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto isDecimalInteger = [](const String& text) {
|
||||||
|
return !text.empty() && std::all_of(text.begin(), text.end(), [](char ch) {
|
||||||
|
return ch >= '0' && ch <= '9';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
|
const SizeT count = tokens.size();
|
||||||
|
|
||||||
|
// Pass 1: for every identifier, the largest literal index it is subscripted with (as a
|
||||||
|
// count, i.e. index + 1), or -1 once it is subscripted with anything that is not a literal.
|
||||||
|
// The declaration's own empty `[]` is neither.
|
||||||
|
MobileGL::UnorderedMap<String, long long> subscriptExtent;
|
||||||
|
for (SizeT i = 1; i < count; ++i) {
|
||||||
|
if (tokens[i].text != "[" || !IsIdentifierToken(tokens[i - 1])) continue;
|
||||||
|
if (i + 1 < count && tokens[i + 1].text == "]") continue; // the unsized declarator itself
|
||||||
|
long long& extent = subscriptExtent[tokens[i - 1].text];
|
||||||
|
if (i + 2 < count && isDecimalInteger(tokens[i + 1].text) && tokens[i + 2].text == "]") {
|
||||||
|
if (extent >= 0) {
|
||||||
|
extent = std::max(extent, std::strtoll(tokens[i + 1].text.c_str(), nullptr, 10) + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extent = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: one edit per repairable member, applied back to front so earlier offsets stand.
|
||||||
|
struct SizeEdit {
|
||||||
|
SizeT pos;
|
||||||
|
String text;
|
||||||
|
};
|
||||||
|
Vector<SizeEdit> edits;
|
||||||
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
|
if (tokens[i].text != "buffer") continue;
|
||||||
|
SizeT cursor = i + 1;
|
||||||
|
// `buffer` is also a member MEMORY qualifier ("buffer vec4 position0;"), which is why
|
||||||
|
// the block body has to be found rather than assumed.
|
||||||
|
if (cursor < count && IsIdentifierToken(tokens[cursor])) ++cursor;
|
||||||
|
if (cursor >= count || tokens[cursor].text != "{") continue;
|
||||||
|
|
||||||
|
const SizeT bodyBegin = cursor + 1;
|
||||||
|
SizeT bodyEnd = bodyBegin;
|
||||||
|
int depth = 1;
|
||||||
|
while (bodyEnd < count) {
|
||||||
|
if (tokens[bodyEnd].text == "{") {
|
||||||
|
++depth;
|
||||||
|
} else if (tokens[bodyEnd].text == "}") {
|
||||||
|
--depth;
|
||||||
|
if (depth == 0) break;
|
||||||
|
}
|
||||||
|
++bodyEnd;
|
||||||
|
}
|
||||||
|
if (depth != 0) continue; // unterminated; glslang will have the last word
|
||||||
|
|
||||||
|
Vector<std::pair<SizeT, SizeT>> members; // [begin, end) of each member, ';' excluded
|
||||||
|
SizeT memberBegin = bodyBegin;
|
||||||
|
for (SizeT m = bodyBegin; m < bodyEnd; ++m) {
|
||||||
|
if (tokens[m].text != ";") continue;
|
||||||
|
members.emplace_back(memberBegin, m);
|
||||||
|
memberBegin = m + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The LAST member is deliberately untouched: an unsized array there is a run-time
|
||||||
|
// sized array, which is both legal and correctly laid out already.
|
||||||
|
for (SizeT index = 0; index + 1 < members.size(); ++index) {
|
||||||
|
const SizeT begin = members[index].first;
|
||||||
|
const SizeT end = members[index].second;
|
||||||
|
if (end < begin + 3) continue;
|
||||||
|
if (tokens[end - 1].text != "]" || tokens[end - 2].text != "[") continue;
|
||||||
|
if (!IsIdentifierToken(tokens[end - 3])) continue;
|
||||||
|
// A multi-declarator member would need one size per declarator; out of scope.
|
||||||
|
bool multipleDeclarators = false;
|
||||||
|
for (SizeT t = begin; t < end; ++t) {
|
||||||
|
if (tokens[t].text == ",") multipleDeclarators = true;
|
||||||
|
}
|
||||||
|
if (multipleDeclarators) continue;
|
||||||
|
const auto known = subscriptExtent.find(tokens[end - 3].text);
|
||||||
|
if (known == subscriptExtent.end() || known->second <= 0) continue;
|
||||||
|
edits.push_back({tokens[end - 1].begin, std::to_string(known->second)});
|
||||||
|
}
|
||||||
|
i = bodyEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
|
||||||
|
source.insert(it->pos, it->text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
|
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
|
||||||
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
|
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
|
||||||
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
|
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
|
||||||
@@ -962,6 +1073,11 @@ namespace MobileGL {
|
|||||||
|
|
||||||
FilterUnsupportedGpuShaderInt64(env, source);
|
FilterUnsupportedGpuShaderInt64(env, source);
|
||||||
CoerceUniformBlockPackingToStd140(source);
|
CoerceUniformBlockPackingToStd140(source);
|
||||||
|
// After the packing coercion: that one rewrites `packed`/`shared` in place and so
|
||||||
|
// cannot move an offset this pass depends on, and reading the block declarations
|
||||||
|
// once both qualifiers are normalized keeps the two passes' notions of a block
|
||||||
|
// declaration identical.
|
||||||
|
SizeNonFinalUnsizedBufferBlockMembers(source);
|
||||||
|
|
||||||
RenameBuiltinShadowingFunctions(source);
|
RenameBuiltinShadowingFunctions(source);
|
||||||
|
|
||||||
@@ -1113,10 +1229,65 @@ namespace MobileGL {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsDecimalIntegerToken(const String& text) {
|
// One GLSL integer literal, spelled the C way: "0x"/"0X" is hexadecimal, a leading
|
||||||
if (text.empty()) return false;
|
// '0' is OCTAL, everything else decimal, and a single trailing 'u'/'U' is legal.
|
||||||
return std::all_of(text.begin(), text.end(),
|
// strtoll with base 0 already implements exactly that detection, so the only work
|
||||||
[](char ch) { return ch >= '0' && ch <= '9'; });
|
// here is deciding what the tail is allowed to be.
|
||||||
|
//
|
||||||
|
// Never guesses, which is the discipline every caller depends on: a float ("1.0"),
|
||||||
|
// an unknown suffix ("3f"), an out-of-range run and a negative value all return
|
||||||
|
// false, and the caller skips the declaration rather than recording a wrong number.
|
||||||
|
bool ParseGlslIntegerLiteral(const String& text, long long& out) {
|
||||||
|
if (text.empty() || text.front() < '0' || text.front() > '9') return false;
|
||||||
|
errno = 0;
|
||||||
|
char* tail = nullptr;
|
||||||
|
const long long value = std::strtoll(text.c_str(), &tail, 0);
|
||||||
|
if (tail == text.c_str() || errno == ERANGE || value < 0) return false;
|
||||||
|
const String suffix = text.substr(static_cast<SizeT>(tail - text.c_str()));
|
||||||
|
if (!suffix.empty() && suffix != "u" && suffix != "U") return false;
|
||||||
|
out = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
|
||||||
|
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
|
||||||
|
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
|
||||||
|
// linker resolves such a name by stripping the single trailing "[0]", so it looks
|
||||||
|
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
|
||||||
|
// silently loses its explicit location.
|
||||||
|
//
|
||||||
|
// Emit those pre-flattened keys here, next to the root, so the result is
|
||||||
|
// order-independent: each carries the location its own element starts at (element
|
||||||
|
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
|
||||||
|
// contain brackets, so a synthesized key never collides with a real uniform name,
|
||||||
|
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
|
||||||
|
void RecordArrayOfArraysElementLocations(const String& name, const Vector<long long>& dimensions,
|
||||||
|
long long baseLocation,
|
||||||
|
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
|
||||||
|
if (dimensions.size() < 2) return;
|
||||||
|
// A pathological declaration must not be able to blow up the map; past the cap
|
||||||
|
// only the root entry stands, which is what every case used to get.
|
||||||
|
constexpr long long kMaxSynthesizedKeys = 4096;
|
||||||
|
const long long innerSpan = dimensions.back();
|
||||||
|
const SizeT outerDimensions = dimensions.size() - 1;
|
||||||
|
long long elementCount = 1;
|
||||||
|
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||||
|
elementCount *= dimensions[d];
|
||||||
|
if (elementCount > kMaxSynthesizedKeys) return;
|
||||||
|
}
|
||||||
|
for (long long element = 0; element < elementCount; ++element) {
|
||||||
|
String key = name;
|
||||||
|
long long remainder = element;
|
||||||
|
for (SizeT d = 0; d < outerDimensions; ++d) {
|
||||||
|
long long stride = 1;
|
||||||
|
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
|
||||||
|
key += "[" + std::to_string(remainder / stride) + "]";
|
||||||
|
remainder %= stride;
|
||||||
|
}
|
||||||
|
locations.emplace(key, static_cast<MobileGL::Int>(
|
||||||
|
std::min(baseLocation + element * innerSpan,
|
||||||
|
static_cast<long long>(INT_MAX / 2))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses one brace-free depth-0 statement [begin, end) and records its
|
// Parses one brace-free depth-0 statement [begin, end) and records its
|
||||||
@@ -1129,6 +1300,7 @@ namespace MobileGL {
|
|||||||
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
|
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
|
||||||
using MobileGL::Int;
|
using MobileGL::Int;
|
||||||
long long location = -1;
|
long long location = -1;
|
||||||
|
long long literal = 0;
|
||||||
bool sawUniform = false;
|
bool sawUniform = false;
|
||||||
SizeT declaratorBegin = end;
|
SizeT declaratorBegin = end;
|
||||||
|
|
||||||
@@ -1144,9 +1316,9 @@ namespace MobileGL {
|
|||||||
} else if (layoutToken == ")") {
|
} else if (layoutToken == ")") {
|
||||||
--parenDepth;
|
--parenDepth;
|
||||||
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
|
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
|
||||||
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
|
tokens[j + 1].text == "=" &&
|
||||||
location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
|
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||||
static_cast<long long>(INT_MAX / 2));
|
location = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||||
j += 2;
|
j += 2;
|
||||||
}
|
}
|
||||||
++j;
|
++j;
|
||||||
@@ -1175,21 +1347,25 @@ namespace MobileGL {
|
|||||||
const String& name = tokens[k].text;
|
const String& name = tokens[k].text;
|
||||||
++k;
|
++k;
|
||||||
long long span = 1;
|
long long span = 1;
|
||||||
|
Vector<long long> dimensions;
|
||||||
while (k < end && tokens[k].text == "[") {
|
while (k < end && tokens[k].text == "[") {
|
||||||
++k;
|
++k;
|
||||||
long long dimension = 1;
|
long long dimension = 1;
|
||||||
if (k < end && IsDecimalIntegerToken(tokens[k].text)) {
|
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
|
||||||
dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10);
|
dimension = literal;
|
||||||
++k;
|
++k;
|
||||||
}
|
}
|
||||||
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
||||||
++k;
|
++k;
|
||||||
span *= std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2)));
|
dimensions.push_back(
|
||||||
|
std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2))));
|
||||||
|
span *= dimensions.back();
|
||||||
}
|
}
|
||||||
// Keep the first sighting: a duplicate can only come from alternative
|
// Keep the first sighting: a duplicate can only come from alternative
|
||||||
// preprocessor branches declaring the same name.
|
// preprocessor branches declaring the same name.
|
||||||
locations.emplace(name, static_cast<Int>(std::min(
|
locations.emplace(name, static_cast<Int>(std::min(
|
||||||
nextLocation, static_cast<long long>(INT_MAX / 2))));
|
nextLocation, static_cast<long long>(INT_MAX / 2))));
|
||||||
|
RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations);
|
||||||
nextLocation += span;
|
nextLocation += span;
|
||||||
if (k >= end) break;
|
if (k >= end) break;
|
||||||
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
|
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
|
||||||
@@ -1225,6 +1401,7 @@ namespace MobileGL {
|
|||||||
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
|
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
|
||||||
using MobileGL::Int;
|
using MobileGL::Int;
|
||||||
long long binding = -1;
|
long long binding = -1;
|
||||||
|
long long literal = 0;
|
||||||
bool sawUniform = false;
|
bool sawUniform = false;
|
||||||
SizeT declaratorBegin = end;
|
SizeT declaratorBegin = end;
|
||||||
|
|
||||||
@@ -1240,9 +1417,9 @@ namespace MobileGL {
|
|||||||
} else if (layoutToken == ")") {
|
} else if (layoutToken == ")") {
|
||||||
--parenDepth;
|
--parenDepth;
|
||||||
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
|
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
|
||||||
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
|
tokens[j + 1].text == "=" &&
|
||||||
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
|
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||||
static_cast<long long>(INT_MAX / 2));
|
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||||
j += 2;
|
j += 2;
|
||||||
}
|
}
|
||||||
++j;
|
++j;
|
||||||
@@ -1275,7 +1452,7 @@ namespace MobileGL {
|
|||||||
++k;
|
++k;
|
||||||
while (k < end && tokens[k].text == "[") {
|
while (k < end && tokens[k].text == "[") {
|
||||||
++k;
|
++k;
|
||||||
if (k < end && IsDecimalIntegerToken(tokens[k].text)) ++k;
|
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) ++k;
|
||||||
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
|
||||||
++k;
|
++k;
|
||||||
}
|
}
|
||||||
@@ -1332,6 +1509,100 @@ namespace MobileGL {
|
|||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Binding points a storage-block declaration starting at `bufferPos` occupies.
|
||||||
|
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
|
||||||
|
// form, which declares no block at all); the element count for an instance array,
|
||||||
|
// whose elements take base, base+1, ... (GLSL 4.30 4.4.5). -1 means "the grammar
|
||||||
|
// here is outside this scanner's narrow subset", i.e. do not judge this one.
|
||||||
|
long long StorageBlockBindingPointCount(const Vector<CodeToken>& tokens, SizeT bufferPos,
|
||||||
|
SizeT count) {
|
||||||
|
SizeT k = bufferPos + 1;
|
||||||
|
if (k < count && IsIdentifierToken(tokens[k])) ++k; // block type name
|
||||||
|
if (k >= count || tokens[k].text != "{") return 1;
|
||||||
|
|
||||||
|
MobileGL::Int braceDepth = 0;
|
||||||
|
while (k < count) {
|
||||||
|
if (tokens[k].text == "{") {
|
||||||
|
++braceDepth;
|
||||||
|
} else if (tokens[k].text == "}") {
|
||||||
|
--braceDepth;
|
||||||
|
if (braceDepth == 0) {
|
||||||
|
++k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++k;
|
||||||
|
}
|
||||||
|
if (braceDepth != 0) return -1; // unterminated block: not this scanner's business
|
||||||
|
|
||||||
|
if (k < count && IsIdentifierToken(tokens[k])) ++k; // instance name
|
||||||
|
if (k >= count || tokens[k].text != "[") return 1;
|
||||||
|
long long elementCount = 0;
|
||||||
|
if (k + 2 < count && ParseGlslIntegerLiteral(tokens[k + 1].text, elementCount) &&
|
||||||
|
tokens[k + 2].text == "]") {
|
||||||
|
return std::max<long long>(1, elementCount);
|
||||||
|
}
|
||||||
|
return -1; // sized by an expression, or unsized
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
|
||||||
|
// A backend that advertises nothing has no ceiling to enforce.
|
||||||
|
if (maxBindings <= 0) return std::nullopt;
|
||||||
|
// Fast path: no storage block, nothing to check. Both keywords are required for a
|
||||||
|
// violation to exist, and the pair is absent from almost every shader-pack source.
|
||||||
|
if (source.find("buffer") == String::npos || source.find("binding") == String::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
|
const SizeT count = tokens.size();
|
||||||
|
// The binding the qualifier run currently being scanned declared, -1 for none.
|
||||||
|
// Several layout(...) lists may precede one declaration and the later one wins,
|
||||||
|
// which is the same accumulate-then-consume shape the extractors above use.
|
||||||
|
long long binding = -1;
|
||||||
|
long long literal = 0;
|
||||||
|
for (SizeT pos = 0; pos < count; ++pos) {
|
||||||
|
const String& text = tokens[pos].text;
|
||||||
|
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
|
||||||
|
SizeT j = pos + 2;
|
||||||
|
Int parenDepth = 1;
|
||||||
|
while (j < count && parenDepth > 0) {
|
||||||
|
const String& layoutToken = tokens[j].text;
|
||||||
|
if (layoutToken == "(") {
|
||||||
|
++parenDepth;
|
||||||
|
} else if (layoutToken == ")") {
|
||||||
|
--parenDepth;
|
||||||
|
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
|
||||||
|
tokens[j + 1].text == "=" &&
|
||||||
|
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
|
||||||
|
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
|
||||||
|
j += 2;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
pos = j - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (text == "buffer") {
|
||||||
|
const long long points = binding >= 0 ? StorageBlockBindingPointCount(tokens, pos, count) : -1;
|
||||||
|
if (points > 0 && binding + points > static_cast<long long>(maxBindings)) {
|
||||||
|
return "ERROR: invalid value " + std::to_string(binding) +
|
||||||
|
" for layout specifier 'binding': a shader storage block occupying " +
|
||||||
|
std::to_string(points) + " binding point(s) from there passes " +
|
||||||
|
"GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS (" + std::to_string(maxBindings) + ").";
|
||||||
|
}
|
||||||
|
binding = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Qualifiers may sit between the layout list and the `buffer` keyword; anything
|
||||||
|
// else ends the run, so a binding never leaks onto an unrelated declaration.
|
||||||
|
if (!IsNonLayoutQualifierKeyword(text)) binding = -1;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
|
||||||
UnorderedMap<String, Int> locations;
|
UnorderedMap<String, Int> locations;
|
||||||
// Fast path: without the qualifier keyword there is nothing to extract.
|
// Fast path: without the qualifier keyword there is nothing to extract.
|
||||||
|
|||||||
@@ -64,6 +64,17 @@ namespace MobileGL {
|
|||||||
// mapIO can capture them, so they are recovered lexically (same narrow
|
// mapIO can capture them, so they are recovered lexically (same narrow
|
||||||
// grammar discipline as ExtractExplicitUniformLocations).
|
// grammar discipline as ExtractExplicitUniformLocations).
|
||||||
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
|
||||||
|
|
||||||
|
// A shader storage block whose layout(binding = N) reaches or passes
|
||||||
|
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
|
||||||
|
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what
|
||||||
|
// has to fit. glslang cannot raise it for MobileGL: every shader is parsed as a Vulkan
|
||||||
|
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
|
||||||
|
// has no storage-buffer binding field to check against in the first place. Returns the
|
||||||
|
// compile-error text for the first violation, or nullopt for a clean source.
|
||||||
|
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
|
||||||
|
// means "nothing to check against" and every declaration passes.
|
||||||
|
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "UniquifyIoBlockNamesPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
#include "source/util/string_utils.h"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
|
||||||
|
// Which storage classes a block struct is reachable from. A struct seen in both
|
||||||
|
// directions inside ONE module cannot be renamed per direction (there is only
|
||||||
|
// one name to change), so it is skipped rather than guessed at.
|
||||||
|
constexpr Uint32 kSeenAsInput = 1u;
|
||||||
|
constexpr Uint32 kSeenAsOutput = 2u;
|
||||||
|
|
||||||
|
// Every struct type carrying the Block decoration, minus the ones with a builtin
|
||||||
|
// member (gl_PerVertex): those are named by the language, not by the shader, and
|
||||||
|
// renaming one would invent a block no driver knows.
|
||||||
|
std::unordered_set<uint32_t> CollectUserBlockStructIds(IRContext* irContext) {
|
||||||
|
std::unordered_set<uint32_t> blockStructIds;
|
||||||
|
std::unordered_set<uint32_t> builtinStructIds;
|
||||||
|
for (Instruction& annotation : irContext->module()->annotations()) {
|
||||||
|
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) ==
|
||||||
|
spv::Decoration::Block) {
|
||||||
|
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||||
|
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
builtinStructIds.insert(annotation.GetSingleWordInOperand(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (uint32_t builtinStructId : builtinStructIds) {
|
||||||
|
blockStructIds.erase(builtinStructId);
|
||||||
|
}
|
||||||
|
return blockStructIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The block struct an Input/Output variable declares, or 0 when the variable is
|
||||||
|
// not an interface block of the kind this pass renames. Tessellation and geometry
|
||||||
|
// interfaces are arrays of the block struct, so one array level is unwrapped -
|
||||||
|
// the same shape StripUboMemberRelaxedPrecisionPass unwraps for instance-arrayed
|
||||||
|
// uniform blocks.
|
||||||
|
uint32_t GetInterfaceBlockStructId(IRContext* irContext, Instruction& variable,
|
||||||
|
const std::unordered_set<uint32_t>& blockStructIds,
|
||||||
|
spv::StorageClass& outStorageClass) {
|
||||||
|
if (variable.opcode() != spv::Op::OpVariable) return 0;
|
||||||
|
const auto storageClass =
|
||||||
|
static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0));
|
||||||
|
if (storageClass != spv::StorageClass::Input &&
|
||||||
|
storageClass != spv::StorageClass::Output) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||||
|
Instruction* pointerType = defUseMgr->GetDef(variable.type_id());
|
||||||
|
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||||
|
uint32_t pointeeId = pointerType->GetSingleWordInOperand(1);
|
||||||
|
Instruction* pointee = defUseMgr->GetDef(pointeeId);
|
||||||
|
while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray ||
|
||||||
|
pointee->opcode() == spv::Op::OpTypeRuntimeArray)) {
|
||||||
|
pointeeId = pointee->GetSingleWordInOperand(0);
|
||||||
|
pointee = defUseMgr->GetDef(pointeeId);
|
||||||
|
}
|
||||||
|
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) return 0;
|
||||||
|
if (blockStructIds.find(pointeeId) == blockStructIds.end()) return 0;
|
||||||
|
|
||||||
|
outStorageClass = storageClass;
|
||||||
|
return pointeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String FindName(IRContext* irContext, uint32_t id) {
|
||||||
|
for (Instruction& debugInst : irContext->debugs2()) {
|
||||||
|
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||||
|
if (debugInst.GetSingleWordInOperand(0) != id) continue;
|
||||||
|
return debugInst.GetInOperand(1).AsString();
|
||||||
|
}
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces an EXISTING OpName only. A block struct with no name of its own is
|
||||||
|
// one SPIRV-Cross would spell from a fallback, which the consuming stage would
|
||||||
|
// not agree with anyway - leave it alone rather than invent a name for it.
|
||||||
|
Bool ReplaceExistingName(IRContext* irContext, uint32_t id, const String& newName) {
|
||||||
|
for (Instruction& debugInst : irContext->debugs2()) {
|
||||||
|
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||||
|
if (debugInst.GetSingleWordInOperand(0) != id) continue;
|
||||||
|
debugInst.SetInOperand(
|
||||||
|
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(newName));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a real id: "this name reached two different struct types in the same
|
||||||
|
// direction", which is already an illegal shader (glslang refuses to reuse a
|
||||||
|
// block name inside one interface) and which no rename could repair - two
|
||||||
|
// structs would come out with one new name. Both the probe and the rewrite
|
||||||
|
// decline it.
|
||||||
|
constexpr uint32_t kAmbiguousStructId = 0xffffffffu;
|
||||||
|
|
||||||
|
// The module's interface blocks indexed the way both halves of this pass need
|
||||||
|
// them: by name within each direction, plus which directions each struct type
|
||||||
|
// is reached from.
|
||||||
|
struct IoBlockIndex {
|
||||||
|
std::map<String, uint32_t> inputStructByName;
|
||||||
|
std::map<String, uint32_t> outputStructByName;
|
||||||
|
std::unordered_map<uint32_t, Uint32> storageMaskByStructId;
|
||||||
|
};
|
||||||
|
|
||||||
|
IoBlockIndex IndexIoBlocks(IRContext* irContext,
|
||||||
|
const std::unordered_set<uint32_t>& blockStructIds) {
|
||||||
|
IoBlockIndex index;
|
||||||
|
for (Instruction& variable : irContext->module()->types_values()) {
|
||||||
|
spv::StorageClass storageClass = spv::StorageClass::Input;
|
||||||
|
const uint32_t structId =
|
||||||
|
GetInterfaceBlockStructId(irContext, variable, blockStructIds, storageClass);
|
||||||
|
if (structId == 0) continue;
|
||||||
|
const Bool isInput = storageClass == spv::StorageClass::Input;
|
||||||
|
index.storageMaskByStructId[structId] |= isInput ? kSeenAsInput : kSeenAsOutput;
|
||||||
|
|
||||||
|
const String blockName = FindName(irContext, structId);
|
||||||
|
if (blockName.empty()) continue;
|
||||||
|
std::map<String, uint32_t>& byName =
|
||||||
|
isInput ? index.inputStructByName : index.outputStructByName;
|
||||||
|
const auto inserted = byName.emplace(blockName, structId);
|
||||||
|
if (!inserted.second && inserted.first->second != structId) {
|
||||||
|
inserted.first->second = kAmbiguousStructId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void UniquifyIoBlockNamesPass::ProbeIoBlockNames(spvtools::opt::IRContext* irContext,
|
||||||
|
std::set<String>& outCollidingBlockNames,
|
||||||
|
std::set<String>& outDeclaredNames) {
|
||||||
|
if (irContext == nullptr) return;
|
||||||
|
|
||||||
|
for (Instruction& debugInst : irContext->debugs2()) {
|
||||||
|
if (debugInst.opcode() != spv::Op::OpName) continue;
|
||||||
|
outDeclaredNames.insert(debugInst.GetInOperand(1).AsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
|
||||||
|
if (blockStructIds.empty()) return;
|
||||||
|
|
||||||
|
const IoBlockIndex index = IndexIoBlocks(irContext, blockStructIds);
|
||||||
|
for (const auto& input : index.inputStructByName) {
|
||||||
|
const auto output = index.outputStructByName.find(input.first);
|
||||||
|
if (output == index.outputStructByName.end()) continue;
|
||||||
|
if (input.second == kAmbiguousStructId || output->second == kAmbiguousStructId) continue;
|
||||||
|
// Same struct type on both sides: there is one name to rename and two
|
||||||
|
// directions wanting different ones, so the collision cannot be repaired.
|
||||||
|
if (input.second == output->second) continue;
|
||||||
|
outCollidingBlockNames.insert(input.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status UniquifyIoBlockNamesPass::Process() {
|
||||||
|
if (m_inputBlockRenames.empty() && m_outputBlockRenames.empty()) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* irContext = context();
|
||||||
|
const std::unordered_set<uint32_t> blockStructIds = CollectUserBlockStructIds(irContext);
|
||||||
|
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
// Indexed BEFORE anything is renamed, so every decline below is decided against
|
||||||
|
// the names the module arrived with rather than against a half-renamed one.
|
||||||
|
const IoBlockIndex index = IndexIoBlocks(irContext, blockStructIds);
|
||||||
|
|
||||||
|
Bool modified = false;
|
||||||
|
for (int direction = 0; direction < 2; ++direction) {
|
||||||
|
const Bool isInput = direction == 0;
|
||||||
|
const std::map<String, uint32_t>& byName =
|
||||||
|
isInput ? index.inputStructByName : index.outputStructByName;
|
||||||
|
const std::map<String, String>& renames =
|
||||||
|
isInput ? m_inputBlockRenames : m_outputBlockRenames;
|
||||||
|
const Uint32 wantedMask = isInput ? kSeenAsInput : kSeenAsOutput;
|
||||||
|
|
||||||
|
for (const auto& block : byName) {
|
||||||
|
if (block.second == kAmbiguousStructId) continue;
|
||||||
|
const auto rename = renames.find(block.first);
|
||||||
|
if (rename == renames.end()) continue;
|
||||||
|
if (rename->second.empty() || rename->second == block.first) continue;
|
||||||
|
// A struct type reached from BOTH directions carries one name for two
|
||||||
|
// interfaces, so renaming it for this direction would rename it for the
|
||||||
|
// other one too. Leave the module as it was.
|
||||||
|
const auto mask = index.storageMaskByStructId.find(block.second);
|
||||||
|
if (mask == index.storageMaskByStructId.end() || mask->second != wantedMask) continue;
|
||||||
|
if (!ReplaceExistingName(irContext, block.second, rename->second)) continue;
|
||||||
|
|
||||||
|
if (m_renamedBlockNames != nullptr) m_renamedBlockNames->insert(block.first);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken UniquifyIoBlockNamesPass::CreateUniquifyIoBlockNamesPass(
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames, std::set<String>* renamedBlockNames) {
|
||||||
|
return spvtools::Optimizer::PassToken(MakeUnique<UniquifyIoBlockNamesPass>(
|
||||||
|
inputBlockRenames, outputBlockRenames, renamedBlockNames));
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.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 "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Renames the STRUCT of an inter-stage interface block, so a block name a stage
|
||||||
|
// declares in both directions at once gets one spelling per producing stage.
|
||||||
|
//
|
||||||
|
// WHY. Desktop GLSL keeps SEPARATE name namespaces for input and output interface
|
||||||
|
// blocks, so a single stage may legally write
|
||||||
|
//
|
||||||
|
// in TCSOutputBlock { ... } input_block[];
|
||||||
|
// out TCSOutputBlock { ... } output_block;
|
||||||
|
//
|
||||||
|
// which is exactly what the tessellation evaluation stage of
|
||||||
|
// KHR-GL42/43.shading_language_420pack.length_of_vector_and_matrix_* and
|
||||||
|
// .qualifier_order_block_* does. glslang accepts it deliberately (ParseHelper
|
||||||
|
// errors only when the two share a storage qualifier) and SPIRV-Cross re-emits
|
||||||
|
// BOTH under the name TCSOutputBlock, because it too splits the namespace
|
||||||
|
// (block_input_names vs block_output_names). The generated ESSL 3.20 then declares
|
||||||
|
// two different blocks called TCSOutputBlock in one shader. Adreno's ES compiler
|
||||||
|
// keeps them apart; Mali's does not - the stage compiles, the program links, and
|
||||||
|
// the output block's payload never reaches the next stage, which is all 22 of
|
||||||
|
// that group's Mali failures and none of Adreno's or DirectVulkan's.
|
||||||
|
//
|
||||||
|
// WHAT. The rename is planned program-wide by the CALLER and keyed on the
|
||||||
|
// PRODUCING stage, so a producer and its consumer keep naming the same block:
|
||||||
|
// the tessellation control stage's `out TCSOutputBlock` and the evaluation
|
||||||
|
// stage's `in TCSOutputBlock` both become <name>_mgio<TCS>, while the evaluation
|
||||||
|
// stage's own `out TCSOutputBlock` and the geometry stage's `in TCSOutputBlock`
|
||||||
|
// both become <name>_mgio<TES>. Only the block TYPE name changes; instance names,
|
||||||
|
// member names, locations and every decoration are left exactly as they were, and
|
||||||
|
// ES matches inter-stage blocks by block name plus member sequence.
|
||||||
|
//
|
||||||
|
// DirectGLES only: DirectVulkan hands the module to the driver as SPIR-V, where
|
||||||
|
// the two blocks are distinct type ids and the debug names carry no meaning.
|
||||||
|
class UniquifyIoBlockNamesPass : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
// `inputBlockRenames` applies to blocks this stage CONSUMES and
|
||||||
|
// `outputBlockRenames` to blocks it PRODUCES, both keyed by the block's
|
||||||
|
// current name. `renamedBlockNames` receives the ORIGINAL names this stage
|
||||||
|
// actually rewrote, so the caller can adopt the re-serialised module only
|
||||||
|
// when there was something to rewrite.
|
||||||
|
UniquifyIoBlockNamesPass(const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
std::set<String>* renamedBlockNames)
|
||||||
|
: m_inputBlockRenames(inputBlockRenames), m_outputBlockRenames(outputBlockRenames),
|
||||||
|
m_renamedBlockNames(renamedBlockNames) {}
|
||||||
|
|
||||||
|
const char* name() const override { return "mobilegl-uniquify-io-block-names"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
// Reads a module WITHOUT rewriting it, for the caller's gate. Adds to
|
||||||
|
// `outCollidingBlockNames` every block name this module declares in BOTH Input
|
||||||
|
// and Output storage under two DIFFERENT struct types - the only shape the
|
||||||
|
// rename above can repair - and to `outDeclaredNames` every name the module
|
||||||
|
// spells, so the caller can pick a replacement that collides with none of them.
|
||||||
|
// Builtin blocks (gl_PerVertex and friends) are never reported.
|
||||||
|
static void ProbeIoBlockNames(spvtools::opt::IRContext* irContext,
|
||||||
|
std::set<String>& outCollidingBlockNames,
|
||||||
|
std::set<String>& outDeclaredNames);
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateUniquifyIoBlockNamesPass(
|
||||||
|
const std::map<String, String>& inputBlockRenames,
|
||||||
|
const std::map<String, String>& outputBlockRenames,
|
||||||
|
std::set<String>* renamedBlockNames);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<String, String> m_inputBlockRenames;
|
||||||
|
std::map<String, String> m_outputBlockRenames;
|
||||||
|
std::set<String>* m_renamedBlockNames = nullptr;
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -326,6 +326,55 @@ namespace MobileGL {
|
|||||||
SPVC_CHK_RETURN
|
SPVC_CHK_RETURN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "gl_AtomicCounterBlock_5" -> 5, -1 for anything that is not one of those blocks.
|
||||||
|
// The suffix is the GL atomic-counter binding the application declared, and after
|
||||||
|
// the relaxed lowering it is the only place that number still exists.
|
||||||
|
static Int AtomicCounterBlockBinding(const char* blockName) {
|
||||||
|
if (blockName == nullptr) return -1;
|
||||||
|
const SizeT prefixLength = std::strlen(ATOMIC_COUNTER_BLOCK_PREFIX);
|
||||||
|
const String name = blockName;
|
||||||
|
if (name.length() <= prefixLength + 1) return -1;
|
||||||
|
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
|
||||||
|
if (name[prefixLength] != '_') return -1;
|
||||||
|
Int binding = 0;
|
||||||
|
for (SizeT i = prefixLength + 1; i < name.length(); ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') return -1;
|
||||||
|
binding = binding * 10 + (name[i] - '0');
|
||||||
|
if (binding > 0x0FFFFFFF) return -1;
|
||||||
|
}
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvc_result SpvcSession::SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings) {
|
||||||
|
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
SPVC_CHK_INIT
|
||||||
|
const spvc_reflected_resource* list = nullptr;
|
||||||
|
size_t count = 0;
|
||||||
|
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
|
||||||
|
resources, SPVC_RESOURCE_TYPE_STORAGE_BUFFER, &list, &count));
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
auto& resource = list[i];
|
||||||
|
// The block TYPE name: glslang gives the synthesized block an EMPTY instance
|
||||||
|
// name, so resource.name carries nothing to match on. Read before Compile(),
|
||||||
|
// which is where SPIRV-Cross renames the reserved "gl_" prefix away.
|
||||||
|
const Int glBinding = AtomicCounterBlockBinding(
|
||||||
|
spvc_compiler_get_name(compiler, resource.base_type_id));
|
||||||
|
if (glBinding < 0) continue;
|
||||||
|
const Int esslBinding = topBinding - glBinding;
|
||||||
|
if (esslBinding < 0) {
|
||||||
|
MGLOG_E_ONCE("Atomic counter binding %d needs more shader storage binding points than this "
|
||||||
|
"driver has; its counters will not be updated.",
|
||||||
|
glBinding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spvc_compiler_set_decoration(compiler, resource.id, SpvDecorationBinding,
|
||||||
|
static_cast<unsigned>(esslBinding));
|
||||||
|
outGlBindings.push_back(glBinding);
|
||||||
|
}
|
||||||
|
SPVC_CHK_RETURN
|
||||||
|
}
|
||||||
|
|
||||||
spvc_result SpvcSession::Compile(const char** result) {
|
spvc_result SpvcSession::Compile(const char** result) {
|
||||||
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
|
||||||
SPVC_CHK_INIT
|
SPVC_CHK_INIT
|
||||||
|
|||||||
@@ -105,6 +105,21 @@ namespace MobileGL {
|
|||||||
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
|
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
|
||||||
// Entries with a negative value mean "never rebound" and are skipped.
|
// Entries with a negative value mean "never rebound" and are skipped.
|
||||||
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings);
|
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings);
|
||||||
|
// Points every synthesized atomic-counter block at a RESERVED storage-block
|
||||||
|
// binding and reports which GL atomic-counter bindings the module declares.
|
||||||
|
//
|
||||||
|
// glslang's relaxed parse rewrote each atomic_uint into a member of
|
||||||
|
// gl_AtomicCounterBlock_<N>, where N is the GL binding the application declared;
|
||||||
|
// the block itself was then auto-mapped to whatever storage-block binding was
|
||||||
|
// free, which has no relation to N and can collide with an SSBO the application
|
||||||
|
// binds itself. Slot N is taken from the TOP of the driver's range downwards
|
||||||
|
// (`topBinding - N`) so the reserved window never overlaps the low bindings
|
||||||
|
// applications use, and a block whose slot would be negative is left alone and
|
||||||
|
// NOT reported - the caller binds nothing there rather than aliasing.
|
||||||
|
//
|
||||||
|
// `outGlBindings` is appended to, so one vector can collect a whole program's
|
||||||
|
// stages; it may repeat a binding declared by several of them.
|
||||||
|
spvc_result SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings);
|
||||||
spvc_result Compile(const char** result);
|
spvc_result Compile(const char** result);
|
||||||
const SpvcMetadata& GetMetadata() const;
|
const SpvcMetadata& GetMetadata() const;
|
||||||
const char* GetLastErrorString() const;
|
const char* GetLastErrorString() const;
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "TranslationCache.h"
|
||||||
|
|
||||||
|
#include <Config.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
// Tags keep two different key builders from ever producing the same blob,
|
||||||
|
// even if their inputs happened to serialize identically.
|
||||||
|
constexpr Uint32 kSpirvKeyTag = 0x4d474c31u; // "MGL1"
|
||||||
|
constexpr Uint32 kEsslKeyTag = 0x4d474c32u; // "MGL2"
|
||||||
|
constexpr Uint32 kParseVerdictKeyTag = 0x4d474c43u; // "MGLC" - L1c, the compile half
|
||||||
|
|
||||||
|
// Bumped whenever the SHAPE of a key changes (a field added, a field's
|
||||||
|
// meaning changed). It is in every blob, so a stale in-memory entry from a
|
||||||
|
// previous shape cannot be honoured - and a future disk tier gets the same
|
||||||
|
// protection for free.
|
||||||
|
//
|
||||||
|
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
||||||
|
// prints it into the emitted ESSL), and L1c was added.
|
||||||
|
// 3: L2 gained the two interface-block rename maps (wave4's UniquifyIoBlockNames).
|
||||||
|
constexpr Uint32 kKeyLayoutVersion = 3u;
|
||||||
|
|
||||||
|
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
||||||
|
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
||||||
|
// cache - one process cannot hold two of them - but it is the knob a disk
|
||||||
|
// tier would have to turn, and putting it in now means the blob format does
|
||||||
|
// not have to change when that tier arrives.
|
||||||
|
void AppendCommonKeyPrefix(TranslationKeyBuilder& builder, const Uint32 tag) {
|
||||||
|
builder.Value(tag);
|
||||||
|
builder.Value(kKeyLayoutVersion);
|
||||||
|
builder.Value(MG_Config::CacheVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- L2 caps -------------------------------------------------------
|
||||||
|
// 128 entries / 12 MiB. Same reasoning, twice the entry count: L2 is keyed
|
||||||
|
// per STAGE rather than per program, so the same program population needs
|
||||||
|
// roughly twice the slots. The byte budget stays put - an L2 entry (SPIR-V
|
||||||
|
// in, ESSL text out) is smaller than an L1 one (all stages' source in, all
|
||||||
|
// stages' SPIR-V out).
|
||||||
|
constexpr SizeT kEsslCacheMaxEntries = 128;
|
||||||
|
constexpr SizeT kEsslCacheMaxBytes = 12u * 1024u * 1024u;
|
||||||
|
|
||||||
|
// ---- L1c caps ------------------------------------------------------
|
||||||
|
// 256 entries / 8 MiB. Per-STAGE like L2, so twice L1's entry count again, and
|
||||||
|
// deliberately generous on count because an L1c entry's PAYLOAD is two words and a
|
||||||
|
// usually-empty string - all of an entry's weight is its key, i.e. the preprocessed
|
||||||
|
// source. 8 MiB is exactly ShaderPreprocessCache's budget, and for the same reason:
|
||||||
|
// these two store the same kind of thing (one copy of a shader's text) and neither
|
||||||
|
// should be the one that decides how much source a process keeps resident.
|
||||||
|
//
|
||||||
|
// Sized for REPETITION, like the other two. A CTS smoke case has fewer than ten
|
||||||
|
// distinct stages and fits many times over; an Iris pack load is hundreds of ~100 KB
|
||||||
|
// mostly-distinct stages that would not hit at any cap, so a bigger budget there buys
|
||||||
|
// nothing and costs resident memory on a phone.
|
||||||
|
constexpr SizeT kParseVerdictCacheMaxEntries = 256;
|
||||||
|
constexpr SizeT kParseVerdictCacheMaxBytes = 8u * 1024u * 1024u;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool ShaderTranslationCacheEnabled() {
|
||||||
|
// Read live rather than latched into a function-local static. MG_Config::Features
|
||||||
|
// is a plain global of scalars written once by MG_ConfigLoader::Init() - a load
|
||||||
|
// costs nothing, no worker ever touches the environment through it, and the unit
|
||||||
|
// tests (which flip the field directly, as AsyncCompileTest and QueryTest already
|
||||||
|
// do) need the switch to actually take effect when they flip it.
|
||||||
|
return MG_Config::Features.ShaderTranslationCache != MG_Config::QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::Bytes(const void* data, const SizeT length) {
|
||||||
|
if (length == 0) return;
|
||||||
|
m_blob.append(static_cast<const char*>(data), length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::Text(const StringView text) {
|
||||||
|
Value(static_cast<Uint64>(text.size()));
|
||||||
|
Bytes(text.data(), text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::Words(const Vector<Uint32>& words) {
|
||||||
|
Value(static_cast<Uint64>(words.size()));
|
||||||
|
Bytes(words.data(), words.size() * sizeof(Uint32));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::TextList(const Vector<String>& values) {
|
||||||
|
Value(static_cast<Uint64>(values.size()));
|
||||||
|
for (const String& value : values) Text(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::StringMap(const std::map<String, String>& map) {
|
||||||
|
Value(static_cast<Uint64>(map.size()));
|
||||||
|
for (const auto& [name, value] : map) {
|
||||||
|
Text(name);
|
||||||
|
Text(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TranslationKeyBuilder::NameSet(const std::set<String>& names) {
|
||||||
|
Value(static_cast<Uint64>(names.size()));
|
||||||
|
for (const String& name : names) Text(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslationCacheKey MakeTranslationCacheKey(String blob) {
|
||||||
|
TranslationCacheKey key;
|
||||||
|
key.hash = static_cast<Uint64>(XXH64(blob.data(), blob.size(), 0));
|
||||||
|
key.blob = MakeShared<const String>(Move(blob));
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslationCacheKey BuildSpirvTranslationKey(const SpirvTranslationKeyInputs& inputs) {
|
||||||
|
TranslationKeyBuilder builder;
|
||||||
|
AppendCommonKeyPrefix(builder, kSpirvKeyTag);
|
||||||
|
builder.Value(inputs.frontendFingerprint);
|
||||||
|
builder.Value(inputs.shaderCompileFlags);
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
||||||
|
builder.Value(static_cast<Uint64>(inputs.stages.size()));
|
||||||
|
for (const auto& stage : inputs.stages) {
|
||||||
|
builder.Value(static_cast<Uint32>(stage.type));
|
||||||
|
builder.Text(stage.preprocessedSource);
|
||||||
|
}
|
||||||
|
static const UnorderedMap<String, Uint> kEmpty;
|
||||||
|
builder.NameMap(inputs.explicitVertexInLocations ? *inputs.explicitVertexInLocations : kEmpty);
|
||||||
|
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
|
||||||
|
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
|
||||||
|
builder.NameMap(inputs.explicitOpaqueUniformBindings ? *inputs.explicitOpaqueUniformBindings : kEmpty);
|
||||||
|
static const Vector<String> kNoXfb;
|
||||||
|
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
|
||||||
|
builder.Value(inputs.xfbBufferMode);
|
||||||
|
builder.Value(inputs.maxFragmentOutputColorNumber);
|
||||||
|
return MakeTranslationCacheKey(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslationCacheKey BuildShaderParseVerdictKey(const ShaderParseVerdictKeyInputs& inputs) {
|
||||||
|
TranslationKeyBuilder builder;
|
||||||
|
AppendCommonKeyPrefix(builder, kParseVerdictKeyTag);
|
||||||
|
builder.Value(inputs.frontendFingerprint);
|
||||||
|
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||||
|
builder.Value(inputs.shaderCompileFlags);
|
||||||
|
builder.Text(inputs.preprocessedSource);
|
||||||
|
return MakeTranslationCacheKey(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) { return verdict.infoLog.size(); }
|
||||||
|
|
||||||
|
// Leaked for the same exit-order reason as the other two; see the note below.
|
||||||
|
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
|
||||||
|
static auto* const kCache = new BoundedTranslationCache<ShaderParseVerdict>(
|
||||||
|
"ShaderTranslationCache L1c (GLSL->parse verdict)", kParseVerdictCacheMaxEntries,
|
||||||
|
kParseVerdictCacheMaxBytes);
|
||||||
|
return *kCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslationCacheKey BuildEsslTranslationKey(const EsslTranslationKeyInputs& inputs) {
|
||||||
|
TranslationKeyBuilder builder;
|
||||||
|
AppendCommonKeyPrefix(builder, kEsslKeyTag);
|
||||||
|
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.supportsViewportArray));
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
||||||
|
builder.Value(inputs.maxColorTextureSamples);
|
||||||
|
builder.Value(inputs.maxIntegerSamples);
|
||||||
|
builder.Value(inputs.maxDepthTextureSamples);
|
||||||
|
builder.Value(inputs.advertisedMaxSamples);
|
||||||
|
builder.Value(static_cast<Uint32>(inputs.esslVersion));
|
||||||
|
builder.Value(inputs.atomicCounterEsslBindingTop);
|
||||||
|
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
||||||
|
static const std::set<String> kEmptySet;
|
||||||
|
builder.NameSet(inputs.xfbCaptureBlockNames ? *inputs.xfbCaptureBlockNames : kEmptySet);
|
||||||
|
static const UnorderedMap<String, Uint> kEmptyFormats;
|
||||||
|
builder.NameMap(inputs.glFormatByUniformName ? *inputs.glFormatByUniformName : kEmptyFormats);
|
||||||
|
static const UnorderedMap<String, Int> kEmptyBindings;
|
||||||
|
builder.NameMap(inputs.storageBlockBindingOverrides ? *inputs.storageBlockBindingOverrides
|
||||||
|
: kEmptyBindings);
|
||||||
|
static const std::map<String, String> kEmptyRenames;
|
||||||
|
builder.StringMap(inputs.inputBlockRenames ? *inputs.inputBlockRenames : kEmptyRenames);
|
||||||
|
builder.StringMap(inputs.outputBlockRenames ? *inputs.outputBlockRenames : kEmptyRenames);
|
||||||
|
static const Vector<Uint32> kEmptyWords;
|
||||||
|
builder.Words(inputs.spirv ? *inputs.spirv : kEmptyWords);
|
||||||
|
return MakeTranslationCacheKey(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT EsslTranslationResultBytes(const EsslTranslationResult& result) {
|
||||||
|
SizeT bytes = result.essl.size();
|
||||||
|
for (const String& name : result.flattenedXfbBlockNames) bytes += name.size();
|
||||||
|
bytes += result.atomicCounterGlBindings.size() * sizeof(Int);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOTH SINGLETONS ARE DELIBERATELY LEAKED, and this is not a style choice - it is the
|
||||||
|
// fix for a crash that reproduced 25 times in 40 runs of AsyncCompileTest.
|
||||||
|
//
|
||||||
|
// A plain function-local static object registers its destructor with __cxa_atexit AT
|
||||||
|
// FIRST USE, and first use here is a ShaderCompilePool worker running the first phase B.
|
||||||
|
// ShaderCompilePool registers its own atexit drain sentinel at FIRST POOL USE, which is
|
||||||
|
// strictly earlier - and exit handlers run in REVERSE registration order. So the cache
|
||||||
|
// would be destroyed FIRST, while workers are still live, and the next worker to reach
|
||||||
|
// Insert() would write into a freed std::list and a freed mutex. The observed symptom
|
||||||
|
// was not a crash in the cache at all: it was heap corruption surfacing later, inside
|
||||||
|
// spirv-tools' AggressiveDCEPass destructor on the worker thread.
|
||||||
|
//
|
||||||
|
// This is the same exit-order hazard PinValidatorTablesForProcessExit documents in
|
||||||
|
// ShaderCompiler.cpp for the validator's lazily-built tables, arriving by the same
|
||||||
|
// route. Pinning the construction order the way that function does would work too, but
|
||||||
|
// leaking is stronger: it holds however late the first phase B happens to run, and a
|
||||||
|
// process-lifetime memo has nothing to release at exit that the OS will not reclaim.
|
||||||
|
//
|
||||||
|
// A function-local static POINTER is trivially destructible, so no exit handler is
|
||||||
|
// registered for it at all. ClearShaderTranslationCaches() is what releases the memory
|
||||||
|
// at a controlled point (eglTerminate), after the pool has been drained.
|
||||||
|
BoundedTranslationCache<EsslTranslationResult>& GetEsslTranslationCache() {
|
||||||
|
static auto* const kCache = new BoundedTranslationCache<EsslTranslationResult>(
|
||||||
|
"ShaderTranslationCache L2 (SPIR-V->ESSL)", kEsslCacheMaxEntries, kEsslCacheMaxBytes);
|
||||||
|
return *kCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearShaderTranslationCaches() {
|
||||||
|
GetShaderParseVerdictCache().Clear();
|
||||||
|
GetEsslTranslationCache().Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogShaderTranslationCacheStats() {
|
||||||
|
GetShaderParseVerdictCache().LogStats();
|
||||||
|
GetEsslTranslationCache().LogStats();
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/TranslationCache.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 <list>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||||
|
// ===========================================================================
|
||||||
|
// The three-level shader translation memo.
|
||||||
|
//
|
||||||
|
// MOTIVATION (measured). KHR-GL33.texture_swizzle.smoke_* builds 2592 programs
|
||||||
|
// per case out of a handful of DISTINCT sources - the CTS template substitutes
|
||||||
|
// BASIC_TYPE and little else within one case - and the process is CPU-bound at
|
||||||
|
// 93% cpu/wall with the device driver's own compiler at 0.15%. Every one of
|
||||||
|
// those 2592 programs walks the whole translation chain again:
|
||||||
|
//
|
||||||
|
// GLSL --[glslang parse]--> AST --[link + mapIO]--> TProgram
|
||||||
|
// --[GlslangToSpv]--> SPIR-V --[SanitizeAndOptimizeBinary]--> SPIR-V'
|
||||||
|
// --[backend SPIR-V pass chain]--> SPIR-V'' --[SPIRV-Cross]--> ESSL
|
||||||
|
//
|
||||||
|
// THE LEVELS FOLLOW THE GL ENTRY POINTS, not the arrows above, and that is the
|
||||||
|
// key to reading this file:
|
||||||
|
// * L1c memoizes what one glCompileShader produces - the PARSE VERDICT;
|
||||||
|
// * L1 memoizes what one glLinkProgram produces - the whole front end from
|
||||||
|
// the link through SPIR-V';
|
||||||
|
// * L2 memoizes the segment from SPIR-V' to the emitted backend payload.
|
||||||
|
//
|
||||||
|
// L1 could never have covered the parse, however wide its payload got, because
|
||||||
|
// the parse does not happen during glLinkProgram: it happens one entry point and
|
||||||
|
// one job earlier, and by the time a link consults L1 it has already been paid
|
||||||
|
// for. That is why the compile half is a separate level rather than a bigger
|
||||||
|
// payload - see the L1c section below for the measurement that forced it.
|
||||||
|
//
|
||||||
|
// L1c and L1 are both backend-agnostic (the same modules feed DirectGLES and
|
||||||
|
// DirectVulkan) and share one environment key, CompileEnv::frontendFingerprint.
|
||||||
|
// L2 is kept apart on purpose: its key is made almost entirely of BACKEND
|
||||||
|
// capability bits, and folding them in would make every DirectGLES capability a
|
||||||
|
// reason to miss on the front-end half as well.
|
||||||
|
//
|
||||||
|
// WITH BOTH FRONT-END LEVELS HIT, NO GLSLANG OBJECT IS CONSTRUCTED AT ALL - no
|
||||||
|
// TShader (L1c) and no TProgram (L1) - so the parse, the link and mapIO,
|
||||||
|
// GlslangToSpv, spirv-opt, buildReflection and the global-UBO routing are all
|
||||||
|
// skipped. On the L1 side that is possible because the payload is the whole
|
||||||
|
// front-end OUTPUT (LinkArtifacts + SpirvArtifacts, both plain owned data)
|
||||||
|
// rather than the SPIR-V alone, and because the GL query surface no longer reads
|
||||||
|
// a live TProgram to answer anything - see ProgramObject::UniformReflection and
|
||||||
|
// ProgramLinkTask::SnapshotGlslangReflection.
|
||||||
|
//
|
||||||
|
// NEITHER LEVEL EVER CACHES A LIVE GLSLANG OBJECT GRAPH, and both had the option:
|
||||||
|
// TObjectReflection::type points into the TProgram's own pool allocator, so
|
||||||
|
// sharing a TProgram between ProgramObjects is an aliasing hazard, and mapIO
|
||||||
|
// mutates a TShader's aliased intermediate, so sharing a parse is a consume-once
|
||||||
|
// hazard. L1 sidesteps the first by storing the reflection as owned data; L1c
|
||||||
|
// sidesteps the second by storing only the VERDICT and letting the one link that
|
||||||
|
// actually needs an AST parse it on demand.
|
||||||
|
//
|
||||||
|
// CORRECTNESS RULE, non-negotiable. A wrong hit is a silently miscompiled
|
||||||
|
// shader - far worse than a slow one. So:
|
||||||
|
// * the key blob carries the FULL bytes of every input, never a digest, and
|
||||||
|
// every candidate hit is confirmed by comparing those bytes. The 64-bit
|
||||||
|
// hash is a bucket selector only; a collision degrades to a miss.
|
||||||
|
// * every input that can change the output is in the blob. Adding an input
|
||||||
|
// to a translation step MEANS adding it to that level's key builder.
|
||||||
|
// * MOBILEGL_SHADER_CACHE=0 turns ALL THREE levels off, so a field miscompile
|
||||||
|
// can be bisected against the cache in one run.
|
||||||
|
//
|
||||||
|
// NO DISK TIER IN THIS CHANGE. Persistence needs its own invalidation story
|
||||||
|
// (driver/vendor string, MobileGL build id, glslang and SPIRV-Cross revisions)
|
||||||
|
// and its own answer to "what if the file is hostile", and neither belongs in
|
||||||
|
// a performance change. Where it WOULD attach: BoundedTranslationCache::Find,
|
||||||
|
// on the miss path, would consult a disk tier keyed by the same blob before
|
||||||
|
// returning null, and Insert would write through to it. Nothing in the design
|
||||||
|
// below forecloses that - the key is already a self-contained byte string and
|
||||||
|
// the payloads are already plain data.
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// The process-wide master switch for ALL THREE levels, mirroring MOBILEGL_SHADER_CACHE.
|
||||||
|
// QuirkOverride semantics: unset (Auto) is ON, an explicitly falsy value is
|
||||||
|
// OFF. Read once from MG_Config::Features, so a worker never touches the
|
||||||
|
// environment.
|
||||||
|
Bool ShaderTranslationCacheEnabled();
|
||||||
|
|
||||||
|
// Serializes the exact bytes of a cache key. Every appender is
|
||||||
|
// length-prefixed or fixed-width, so no two different input tuples can
|
||||||
|
// serialize to the same byte string by running into each other.
|
||||||
|
class TranslationKeyBuilder {
|
||||||
|
public:
|
||||||
|
void Bytes(const void* data, SizeT length);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Value(const T& value) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>,
|
||||||
|
"TranslationKeyBuilder::Value hashes the object representation");
|
||||||
|
Bytes(&value, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length-prefixed, so "ab"+"c" and "a"+"bc" cannot collide.
|
||||||
|
void Text(StringView text);
|
||||||
|
void Words(const Vector<Uint32>& words);
|
||||||
|
|
||||||
|
// Hash maps and sets are serialized in SORTED order, never in iteration
|
||||||
|
// order: ska::flat_hash_map's iteration order depends on insertion history
|
||||||
|
// and capacity, so two logically identical maps could otherwise serialize
|
||||||
|
// differently and cause spurious misses. Sorting makes the blob canonical.
|
||||||
|
// These maps are all tiny (explicit locations, image formats, storage-block
|
||||||
|
// rebindings), so the sort is free.
|
||||||
|
template <typename ValueT>
|
||||||
|
void NameMap(const UnorderedMap<String, ValueT>& map) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<ValueT>);
|
||||||
|
Vector<Pair<StringView, ValueT>> sorted;
|
||||||
|
sorted.reserve(map.size());
|
||||||
|
for (const auto& [name, value] : map) sorted.emplace_back(StringView(name), value);
|
||||||
|
std::sort(sorted.begin(), sorted.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||||
|
Value(static_cast<Uint64>(sorted.size()));
|
||||||
|
for (const auto& [name, value] : sorted) {
|
||||||
|
Text(name);
|
||||||
|
Value(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// std::set is already ordered, but it gets the same length prefix.
|
||||||
|
void NameSet(const std::set<String>& names);
|
||||||
|
// std::map is ordered too, so it needs no sort - but both halves are TEXT, so each
|
||||||
|
// gets its own length prefix and the pair cannot run into the next one.
|
||||||
|
void StringMap(const std::map<String, String>& map);
|
||||||
|
// ORDER-SENSITIVE, unlike NameMap: a transform-feedback capture list is a sequence,
|
||||||
|
// and gl_NextBuffer / gl_SkipComponentsN make its order load-bearing.
|
||||||
|
void TextList(const Vector<String>& values);
|
||||||
|
|
||||||
|
const String& Blob() const { return m_blob; }
|
||||||
|
String Take() { return Move(m_blob); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
String m_blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A cache key: the full bytes, plus the hash that selects a bucket for them.
|
||||||
|
// The blob is shared rather than copied so that indexing an entry by its key
|
||||||
|
// does not double the memory a 100 KB shaderpack stage costs.
|
||||||
|
struct TranslationCacheKey {
|
||||||
|
Uint64 hash = 0;
|
||||||
|
SharedPtr<const String> blob;
|
||||||
|
|
||||||
|
Bool Valid() const { return blob != nullptr; }
|
||||||
|
SizeT Bytes() const { return blob ? blob->size() : 0u; }
|
||||||
|
|
||||||
|
// FULL comparison, always. This is what makes a hash collision a miss
|
||||||
|
// rather than a miscompiled shader.
|
||||||
|
Bool operator==(const TranslationCacheKey& other) const {
|
||||||
|
if (hash != other.hash) return false;
|
||||||
|
if (blob == other.blob) return true; // the same buffer
|
||||||
|
if (!blob || !other.blob) return false;
|
||||||
|
return *blob == *other.blob;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TranslationCacheKeyHasher {
|
||||||
|
SizeT operator()(const TranslationCacheKey& key) const { return static_cast<SizeT>(key.hash); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seals a builder's bytes into a key.
|
||||||
|
TranslationCacheKey MakeTranslationCacheKey(String blob);
|
||||||
|
inline TranslationCacheKey MakeTranslationCacheKey(TranslationKeyBuilder& builder) {
|
||||||
|
return MakeTranslationCacheKey(builder.Take());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TranslationCacheStats {
|
||||||
|
Uint64 hits = 0;
|
||||||
|
Uint64 misses = 0;
|
||||||
|
Uint64 inserts = 0;
|
||||||
|
Uint64 evictions = 0;
|
||||||
|
// Entries whose own key+payload already exceed the whole byte budget.
|
||||||
|
// Caching one would evict everything else and then itself.
|
||||||
|
Uint64 rejectedOversize = 0;
|
||||||
|
// Two workers missed on the same key and both computed it. Harmless (the
|
||||||
|
// key covers every input, so both results are equal), but worth counting:
|
||||||
|
// a large number would mean the redundancy is no longer a startup artifact.
|
||||||
|
Uint64 duplicateInserts = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A bounded, thread-safe, process-lifetime memo.
|
||||||
|
//
|
||||||
|
// EVICTION is FIFO, bounded by BOTH an entry count and a stored-byte budget,
|
||||||
|
// whichever binds first - the same policy (and the same reasoning) as
|
||||||
|
// ShaderPreprocessCache. Translation workloads are bursts of mostly-distinct
|
||||||
|
// inputs whose reuse clusters around insertion time, and FIFO keeps Find() a
|
||||||
|
// read-only operation: with N pool workers hammering the same cache, an LRU
|
||||||
|
// splice on every hit would turn the shared hit path into a writer.
|
||||||
|
//
|
||||||
|
// THREAD SAFETY. The mutex guards the containers only; the expensive
|
||||||
|
// translation always runs OUTSIDE it, between the Find and the Insert. Two
|
||||||
|
// workers that miss on the same key therefore both compute it, and the second
|
||||||
|
// Insert is dropped. That is deliberate: the alternative - one worker waits
|
||||||
|
// for the other's result - would block a pool worker inside a job body, which
|
||||||
|
// is precisely the invariant (JobNode I4) that keeps ShaderCompilePool from
|
||||||
|
// deadlocking when the waiting job holds the only worker the awaited job needs.
|
||||||
|
// The waste is bounded by the worker count and only happens on the first burst.
|
||||||
|
//
|
||||||
|
// LIFETIME. Hits hand out shared ownership of the payload, never a pointer into
|
||||||
|
// the entry list, so a reader keeps its payload alive across any concurrent
|
||||||
|
// eviction - and across Clear() and the cache's own destruction.
|
||||||
|
template <typename Payload>
|
||||||
|
class BoundedTranslationCache {
|
||||||
|
public:
|
||||||
|
using PayloadPtr = SharedPtr<const Payload>;
|
||||||
|
|
||||||
|
BoundedTranslationCache(const char* name, SizeT maxEntries, SizeT maxBytes)
|
||||||
|
: m_name(name), m_maxEntries(maxEntries), m_maxBytes(maxBytes) {}
|
||||||
|
|
||||||
|
PayloadPtr Find(const TranslationCacheKey& key) const {
|
||||||
|
if (!key.Valid()) return nullptr;
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
const auto it = m_index.find(key);
|
||||||
|
if (it == m_index.end()) {
|
||||||
|
++m_stats.misses;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
++m_stats.hits;
|
||||||
|
return it->second->payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Insert(TranslationCacheKey key, PayloadPtr payload, SizeT payloadBytes) {
|
||||||
|
if (!key.Valid() || !payload) return;
|
||||||
|
const SizeT entryBytes = key.Bytes() + payloadBytes;
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (entryBytes > m_maxBytes) {
|
||||||
|
++m_stats.rejectedOversize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_index.find(key) != m_index.end()) {
|
||||||
|
// A concurrent miss on the same key computed it too. The incumbent
|
||||||
|
// is kept: the key covers every input, so the two payloads are
|
||||||
|
// equal, and replacing would only move a demonstrably-wanted entry
|
||||||
|
// to the back of the FIFO.
|
||||||
|
++m_stats.duplicateInserts;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_entries.push_back(Entry{key, Move(payload), entryBytes});
|
||||||
|
m_index.emplace(Move(key), std::prev(m_entries.end()));
|
||||||
|
m_storedBytes += entryBytes;
|
||||||
|
++m_stats.inserts;
|
||||||
|
EvictUntilWithinBudgetLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_index.clear();
|
||||||
|
m_entries.clear();
|
||||||
|
m_storedBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslationCacheStats Stats() const {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT EntryCount() const {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT StoredBytes() const {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_storedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MGLOG_D, so an INFO build compiles this out entirely.
|
||||||
|
void LogStats() const {
|
||||||
|
const TranslationCacheStats stats = Stats();
|
||||||
|
const Uint64 lookups = stats.hits + stats.misses;
|
||||||
|
MGLOG_D("%s: %llu/%llu hits (%.1f%%), %llu inserts, %llu evictions, %llu oversize, "
|
||||||
|
"%llu duplicate, %zu entries / %zu KiB",
|
||||||
|
m_name, static_cast<unsigned long long>(stats.hits),
|
||||||
|
static_cast<unsigned long long>(lookups),
|
||||||
|
lookups ? 100.0 * static_cast<double>(stats.hits) / static_cast<double>(lookups) : 0.0,
|
||||||
|
static_cast<unsigned long long>(stats.inserts),
|
||||||
|
static_cast<unsigned long long>(stats.evictions),
|
||||||
|
static_cast<unsigned long long>(stats.rejectedOversize),
|
||||||
|
static_cast<unsigned long long>(stats.duplicateInserts), EntryCount(),
|
||||||
|
StoredBytes() / 1024u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests only: makes the caps small enough to exercise eviction without
|
||||||
|
// building megabytes of shaders. Clears the cache, because shrinking the
|
||||||
|
// caps under live entries would otherwise leave it over budget.
|
||||||
|
void SetCapsForTesting(SizeT maxEntries, SizeT maxBytes) {
|
||||||
|
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_maxEntries = maxEntries;
|
||||||
|
m_maxBytes = maxBytes;
|
||||||
|
m_index.clear();
|
||||||
|
m_entries.clear();
|
||||||
|
m_storedBytes = 0;
|
||||||
|
m_stats = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
TranslationCacheKey key;
|
||||||
|
PayloadPtr payload;
|
||||||
|
SizeT bytes = 0;
|
||||||
|
};
|
||||||
|
using EntryList = std::list<Entry>;
|
||||||
|
|
||||||
|
void EvictUntilWithinBudgetLocked() {
|
||||||
|
while (!m_entries.empty() &&
|
||||||
|
(m_entries.size() > m_maxEntries || m_storedBytes > m_maxBytes)) {
|
||||||
|
const auto victim = m_entries.begin();
|
||||||
|
m_storedBytes -= victim->bytes;
|
||||||
|
m_index.erase(victim->key);
|
||||||
|
m_entries.erase(victim);
|
||||||
|
++m_stats.evictions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* m_name = "";
|
||||||
|
SizeT m_maxEntries = 0;
|
||||||
|
SizeT m_maxBytes = 0;
|
||||||
|
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
mutable TranslationCacheStats m_stats;
|
||||||
|
EntryList m_entries; // front = oldest = FIFO victim
|
||||||
|
UnorderedMap<TranslationCacheKey, typename EntryList::iterator, TranslationCacheKeyHasher> m_index;
|
||||||
|
SizeT m_storedBytes = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// L1 - the LINK half of the front end: parsed GLSL program -> sanitized SPIR-V
|
||||||
|
// modules, plus the whole GL query surface. (The PARSE half is L1c, below.)
|
||||||
|
// =======================================================================
|
||||||
|
//
|
||||||
|
// The cached artifact is the module AFTER SanitizeAndOptimizeBinary, not the
|
||||||
|
// raw GlslangToSpv output. That is a deliberate choice and it is safe:
|
||||||
|
// SanitizeAndOptimizeBinary is a fixed 11-pass spirv-opt chain with no
|
||||||
|
// arguments but the module, and its two remaining parameters (`validateOutput`,
|
||||||
|
// `enableSpirvValidation`) only decide whether the OUTPUT is handed to the
|
||||||
|
// validator and logged - RunOptimizerChecked runs the optimizer first and
|
||||||
|
// identically either way. Nothing between GlslangToSpv and Sanitize reads
|
||||||
|
// backend state. So caching after Sanitize saves the 96 us/stage the chain
|
||||||
|
// costs on top of the 40 us GlslangToSpv, and gives the backends exactly the
|
||||||
|
// bytes they would have got.
|
||||||
|
//
|
||||||
|
// L1 IS BACKEND-AGNOSTIC BY CONTRACT. Two contexts on different GPUs compiling
|
||||||
|
// the same GLSL share one L1 entry: nothing that merely steers a BACKEND
|
||||||
|
// transpile (backend identity, GLES/Vulkan capability bits, driver extension
|
||||||
|
// strings, GPU vendor) is allowed in this key - all of that lives in L2's key,
|
||||||
|
// where it belongs. What IS here is the subset of the environment that changes
|
||||||
|
// what glslang itself produces; see CompileEnv::frontendFingerprint for the
|
||||||
|
// field-by-field classification and the evidence behind each call.
|
||||||
|
//
|
||||||
|
// WHAT IS IN THE KEY (each one is an input that can change the modules):
|
||||||
|
// * CompileEnv::frontendFingerprint - the glslang resource limits
|
||||||
|
// BuildTBuiltInResource enforces at parse, plus the two inputs to the
|
||||||
|
// reflection vertex-attrib limit. NOT CompileEnv::fingerprint, which also
|
||||||
|
// covers backend identity and the advertised extension vector;
|
||||||
|
// * per stage, in link order: the GL stage enum and the FULL preprocessed
|
||||||
|
// source, which is literally the text ParseShaderSource was given;
|
||||||
|
// * the four link-time request maps mapIO resolves against
|
||||||
|
// (glBindAttribLocation / glBindFragDataLocation /
|
||||||
|
// glBindFragDataLocationIndexed, and the merged layout(binding=) opaque
|
||||||
|
// units) - these steer TMglGlslIoResolver and therefore the Locations and
|
||||||
|
// Bindings baked into every module;
|
||||||
|
// * the ShaderCompileBits the parse ran under (always 0 in production; in
|
||||||
|
// the key so a future non-zero value cannot alias);
|
||||||
|
// * the SPIR-V validation switch (byte-identical output either way, but it
|
||||||
|
// costs one byte to be sure).
|
||||||
|
//
|
||||||
|
// The key is a PROGRAM-level key, not a per-stage one, and that is forced:
|
||||||
|
// glslang's mapIO resolves a fragment stage's input Locations against the
|
||||||
|
// vertex stage's outputs, so a stage's SPIR-V is NOT a function of that
|
||||||
|
// stage's source alone. A per-stage key here would be exactly the silent
|
||||||
|
// miscompile this cache must never produce.
|
||||||
|
struct SpirvTranslationKeyInputs {
|
||||||
|
struct Stage {
|
||||||
|
GLenum type = 0;
|
||||||
|
StringView preprocessedSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CompileEnv::frontendFingerprint, NEVER CompileEnv::fingerprint - see the
|
||||||
|
// backend-agnosticism note above.
|
||||||
|
Uint64 frontendFingerprint = 0;
|
||||||
|
Vector<Stage> stages;
|
||||||
|
const UnorderedMap<String, Uint>* explicitVertexInLocations = nullptr;
|
||||||
|
const UnorderedMap<String, Uint>* explicitFragmentOutLocations = nullptr;
|
||||||
|
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
||||||
|
const UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
||||||
|
Uint32 shaderCompileFlags = 0;
|
||||||
|
Bool enableSpirvValidation = false;
|
||||||
|
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
|
||||||
|
// When the payload was SPIR-V alone these were provably irrelevant: transform
|
||||||
|
// feedback is resolved by READING the linked intermediates and never writes an XFB
|
||||||
|
// qualifier, and the fragment-output limit is a link-failure gate, so neither can
|
||||||
|
// move a single word of the generated module. Both DO shape LinkArtifacts
|
||||||
|
// (xfbVaryings / xfbStrides / xfbBufferMode / gsStripTriangles, and whether the link
|
||||||
|
// is rejected at all), so widening the payload to the whole front end pulled them
|
||||||
|
// into the key. Widening a payload means widening the key.
|
||||||
|
const Vector<String>* requestedXfbVaryings = nullptr;
|
||||||
|
Uint32 xfbBufferMode = 0;
|
||||||
|
Int32 maxFragmentOutputColorNumber = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TranslationCacheKey BuildSpirvTranslationKey(const SpirvTranslationKeyInputs& inputs);
|
||||||
|
|
||||||
|
// The L1 PAYLOAD and its cache instance live in
|
||||||
|
// MG_State/GLState/ProgramState/ProgramTranslationCache.h, not here: the payload is a
|
||||||
|
// whole ProgramObject::LinkArtifacts + SpirvArtifacts, and MG_Util must not depend on
|
||||||
|
// MG_State. Only the key - which is plain bytes - is built here, so both layers agree on
|
||||||
|
// one definition of "the same front-end input".
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// L1c - the COMPILE half of the front end: one glCompileShader's PARSE VERDICT.
|
||||||
|
// =======================================================================
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS. L1 above memoizes one glLinkProgram. It skips the link, mapIO,
|
||||||
|
// GlslangToSpv, spirv-opt, buildReflection and the routing pass - but NOT the glslang
|
||||||
|
// parse, because the parse does not happen at glLinkProgram. It happens at
|
||||||
|
// glCompileShader, one job earlier, and by the time the link hits L1 the parse has
|
||||||
|
// already been paid for. Measured: the parse is ~322 us of a ~650 us CTS-shaped program
|
||||||
|
// build, and on a Mali Immortalis-G925 an L1-only build of
|
||||||
|
// KHR-GL33.texture_swizzle.smoke_access_idx_0_channel_idx_0 (2592 programs) ran 50.65 s
|
||||||
|
// against 75.16/72.68 s with the cache off - 1.45-1.48x, which is what "everything but
|
||||||
|
// the parse" buys. This level is the other half.
|
||||||
|
//
|
||||||
|
// WHAT IS MEMOIZED IS THE VERDICT, NOT THE PARSE. glCompileShader produces exactly three
|
||||||
|
// parse-derived things: GL_COMPILE_STATUS, the info log, and a glslang::TShader. The
|
||||||
|
// first two are a pure function of the key below. The third is CONSUME-ONCE - mapIO
|
||||||
|
// mutates its aliased intermediate at link - so it can be neither cached nor shared, and
|
||||||
|
// caching a live glslang object graph was rejected for L1 for exactly that reason.
|
||||||
|
//
|
||||||
|
// So a hit publishes the verdict and NO TShader at all, and the parse becomes LAZY:
|
||||||
|
// ShaderCompileTask::ClaimParsedShader already re-parses on demand when the node carries
|
||||||
|
// no stored parse, because stage 4 built that path for the CAS loser (one shader linked
|
||||||
|
// into a second program). A link that HITS L1 never calls it, so the parse never happens.
|
||||||
|
// A link that MISSES calls it and pays the parse there instead - the same single parse,
|
||||||
|
// moved, not duplicated.
|
||||||
|
//
|
||||||
|
// WHAT THIS DELIBERATELY IS NOT: an extension of ShaderCompileAdoptionMap. That map
|
||||||
|
// indexes LIVE compile nodes by WeakPtr, per context, so that a burst of shader objects
|
||||||
|
// handed byte-identical source shares one job. It structurally cannot serve this case:
|
||||||
|
// the CTS shape deletes its shader objects every iteration, so the node expires and the
|
||||||
|
// entry with it, and even a hit would hand over a parse whose single use the first link
|
||||||
|
// already consumed. Making it hold strong references would pin one glslang arena per
|
||||||
|
// distinct source for the life of the context - megabytes per shaderpack, and precisely
|
||||||
|
// the live-object-graph hazard this design avoids.
|
||||||
|
//
|
||||||
|
// BACKEND-AGNOSTIC, on the same contract as L1: the key carries
|
||||||
|
// CompileEnv::frontendFingerprint and never CompileEnv::fingerprint.
|
||||||
|
//
|
||||||
|
// IF THE KEY IS EVER WRONG, the two directions fail very differently, and it is worth
|
||||||
|
// knowing which one to fear:
|
||||||
|
// * a wrong `parsed = true` is CAUGHT. The stage holds no AST, so the first link that
|
||||||
|
// needs one re-parses - and that parse fails, ConsumeShaders reports "Internal error:
|
||||||
|
// re-parsing an attached <stage> for linking failed" and the link returns GL_FALSE.
|
||||||
|
// Wrong, loud, and named.
|
||||||
|
// * a wrong `parsed = false` is NOT caught. Nothing re-derives it, so a shader that
|
||||||
|
// would have compiled reports GL_COMPILE_STATUS false with a stale log.
|
||||||
|
// Neither is a silent MISCOMPILE - no wrong SPIR-V can be produced through this level,
|
||||||
|
// because it caches no translated output at all - but the second is the one that would
|
||||||
|
// reach an application as an inexplicable failure. Both are why the key carries the full
|
||||||
|
// source bytes and is compared in full.
|
||||||
|
struct ShaderParseVerdict {
|
||||||
|
// What ShaderCompileTask publishes as GL_COMPILE_STATUS.
|
||||||
|
Bool parsed = false;
|
||||||
|
// What it publishes as the info log. EMPTY whenever `parsed`, and that is a property
|
||||||
|
// of the pipeline rather than of glslang: RunCompilePipeline clears the log on a
|
||||||
|
// successful parse, so a successful compile's observable log is empty no matter what
|
||||||
|
// glslang wrote into it. Stored rather than assumed so the two cannot drift.
|
||||||
|
String infoLog;
|
||||||
|
};
|
||||||
|
using ShaderParseVerdictPtr = SharedPtr<const ShaderParseVerdict>;
|
||||||
|
|
||||||
|
// WHAT IS IN THE KEY - the complete input set of ShaderCompiler::CompileShader, which is
|
||||||
|
// the only thing between this cache and the verdict:
|
||||||
|
// * frontendFingerprint - BuildTBuiltInResource is the one thing ParseShaderSource
|
||||||
|
// reads from the environment, and glslang both ENFORCES those limits at parse and
|
||||||
|
// expands several of them into built-in constants;
|
||||||
|
// * shaderType - it selects the EShLanguage parsed against, and it is also printed
|
||||||
|
// verbatim into the failure log this cache reproduces;
|
||||||
|
// * the FULL preprocessed source, byte for byte. This is the text ParseShaderSource is
|
||||||
|
// handed, and it also covers CompileShader's legacy-#version retry, which is a pure
|
||||||
|
// function of that text (RetargetLegacyVersionDirectiveTo460);
|
||||||
|
// * the ShaderCompileBits - CompileForOpenGL selects a different setEnvClient /
|
||||||
|
// setEnvTarget triple and skips setEnvInputVulkanRulesRelaxed, which changes both
|
||||||
|
// what parses and what the parse produces. Always 0 on both production paths; in the
|
||||||
|
// key so a future non-zero value cannot alias a parse made without it.
|
||||||
|
//
|
||||||
|
// WHAT IS DELIBERATELY OUT:
|
||||||
|
// * everything else ParseShaderSource touches, because all of it is a COMPILE-TIME
|
||||||
|
// CONSTANT: the 460/ECoreProfile default version, EShMsgDefault, forwardCompatible,
|
||||||
|
// the "#undef VULKAN" preamble, setNanMinMaxClamp/setInvertY/setAutoMapLocations/
|
||||||
|
// setAutoMapBindings, and GLOBAL_UBO_NAME. A build that changes one of them is a
|
||||||
|
// different binary and cannot share an in-memory cache with the old one.
|
||||||
|
// * enableSpirvValidation. It is not an argument of CompileShader at all - the parse
|
||||||
|
// never reaches the SPIR-V validator. (L1 carries it because SanitizeAndOptimizeBinary
|
||||||
|
// does take it.)
|
||||||
|
// * backend identity and advertisedExtensions, on exactly L1's argument: the only
|
||||||
|
// front-end consumer of the extension list REWRITES THE SOURCE TEXT, and the
|
||||||
|
// preprocessed text is in this key verbatim - a strictly finer discriminator.
|
||||||
|
// * the ORIGINAL (pre-preprocess) source. The preprocessed text is what the parse
|
||||||
|
// consumes, so keying on the original would be both coarser in the wrong direction
|
||||||
|
// and redundant; ShaderPreprocessCache is the memo that keys on the original.
|
||||||
|
struct ShaderParseVerdictKeyInputs {
|
||||||
|
// CompileEnv::frontendFingerprint, NEVER CompileEnv::fingerprint.
|
||||||
|
Uint64 frontendFingerprint = 0;
|
||||||
|
GLenum shaderType = 0;
|
||||||
|
StringView preprocessedSource;
|
||||||
|
Uint32 shaderCompileFlags = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TranslationCacheKey BuildShaderParseVerdictKey(const ShaderParseVerdictKeyInputs& inputs);
|
||||||
|
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict);
|
||||||
|
|
||||||
|
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache();
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// L2 - the BACK END: sanitized SPIR-V -> DirectGLES ESSL payload.
|
||||||
|
// =======================================================================
|
||||||
|
//
|
||||||
|
// DIRECTGLES ONLY. DirectVulkan runs a different pass chain, steered by Vulkan
|
||||||
|
// device features, and gets no L2 in this change; giving it one means giving it
|
||||||
|
// its OWN instance with its OWN key, never this one.
|
||||||
|
//
|
||||||
|
// The memoized segment is BackendProgramObjectImpl::SyncToBackend's per-stage
|
||||||
|
// block from the draw-parameter lowering down to (and including) the
|
||||||
|
// SPIRV-Cross Compile() that produces the ESSL text. The text-level passes that
|
||||||
|
// follow it are deliberately outside: they are cheap string work, and they read
|
||||||
|
// a long tail of live per-program state (RebindImageUniformsToFrontendUnits
|
||||||
|
// walks the ProgramObject's uniform reflection, the norm-clamp masks and the
|
||||||
|
// fragColor broadcast count are live globals) whose inclusion would make the
|
||||||
|
// key both huge and fragile for no measurable saving.
|
||||||
|
//
|
||||||
|
// WHAT IS IN THE KEY:
|
||||||
|
// * the FULL SPIR-V module (the input);
|
||||||
|
// * the GL stage enum - three passes are stage-gated (draw parameters and
|
||||||
|
// array vertex inputs on vertex, fragment-output index legalization on
|
||||||
|
// fragment);
|
||||||
|
// * SupportsViewportArray - arms LowerViewportIndexForEssl;
|
||||||
|
// * the four sample ceilings (color / integer / depth / advertised) - both
|
||||||
|
// ARM ClampMultisampleFetchesForEssl and PARAMETERIZE it;
|
||||||
|
// * SupportsNoperspectiveInterpolation - arms EmulateNoPerspectiveForEssl;
|
||||||
|
// * the transform-feedback capture block names - the argument to
|
||||||
|
// FlattenXfbInterfaceBlocksForEssl, and the reason the payload has to carry
|
||||||
|
// the names it actually flattened;
|
||||||
|
// * the image-format bake map (uniform name -> GL internal format), which is
|
||||||
|
// derived from LIVE glBindImageTexture state and is the one genuinely
|
||||||
|
// per-draw-state input in here;
|
||||||
|
// * the storage-block binding overrides handed to SPIRV-Cross;
|
||||||
|
// * the atomic-counter binding top, which SetAtomicCounterBlockBindings turns into the
|
||||||
|
// layout(binding=) qualifier every synthesized counter block is printed with;
|
||||||
|
// * this stage's two interface-block rename maps, which UniquifyIoBlockNamesForEssl
|
||||||
|
// turns into the block type names the emitted ESSL spells;
|
||||||
|
// * the ESSL version SPIRV-Cross targets (ResolveBackendEsslVersion, i.e. the
|
||||||
|
// driver's GLES version) - the remaining two SPIRV-Cross options are
|
||||||
|
// compile-time constants (GLSL_ES true, VULKAN_SEMANTICS false);
|
||||||
|
// * the SPIR-V validation switch, as in L1.
|
||||||
|
//
|
||||||
|
// Unconditional passes (StripUboMemberRelaxedPrecision, LowerRectImages,
|
||||||
|
// Lower1DArrayImages) take no input but the module and so need no key material.
|
||||||
|
struct EsslTranslationResult {
|
||||||
|
String essl;
|
||||||
|
// Which interface blocks FlattenXfbInterfaceBlocksForEssl actually rewrote
|
||||||
|
// in THIS stage. The caller unions these across stages and the transform-
|
||||||
|
// feedback capture list follows them, so a payload that dropped them would
|
||||||
|
// silently un-rename every capture on a cache hit.
|
||||||
|
std::set<String> flattenedXfbBlockNames;
|
||||||
|
// Which GL atomic-counter binding points THIS stage's synthesized
|
||||||
|
// gl_AtomicCounterBlock_<N> blocks named, as SetAtomicCounterBlockBindings reported
|
||||||
|
// them. Same contract as the XFB names above and here for the same reason: the draw
|
||||||
|
// path re-issues exactly these as storage-buffer bindings, so a payload that dropped
|
||||||
|
// them would leave every counter buffer unbound on a hit - a program that renders but
|
||||||
|
// never increments a counter, which is far harder to notice than a broken shader.
|
||||||
|
Vector<Int> atomicCounterGlBindings;
|
||||||
|
};
|
||||||
|
using EsslTranslationResultPtr = SharedPtr<const EsslTranslationResult>;
|
||||||
|
|
||||||
|
struct EsslTranslationKeyInputs {
|
||||||
|
const Vector<Uint32>* spirv = nullptr;
|
||||||
|
GLenum shaderType = 0;
|
||||||
|
|
||||||
|
// --- driver capability bits that arm or steer a pass ---
|
||||||
|
Bool supportsViewportArray = false;
|
||||||
|
Bool supportsNoperspectiveInterpolation = false;
|
||||||
|
Int32 maxColorTextureSamples = 0;
|
||||||
|
Int32 maxIntegerSamples = 0;
|
||||||
|
Int32 maxDepthTextureSamples = 0;
|
||||||
|
Int32 advertisedMaxSamples = 0;
|
||||||
|
|
||||||
|
// --- per-program / per-context inputs ---
|
||||||
|
const std::set<String>* xfbCaptureBlockNames = nullptr;
|
||||||
|
const UnorderedMap<String, Uint>* glFormatByUniformName = nullptr;
|
||||||
|
const UnorderedMap<String, Int>* storageBlockBindingOverrides = nullptr;
|
||||||
|
// THIS STAGE's share of the program-wide interface-block rename plan - the two
|
||||||
|
// arguments UniquifyIoBlockNamesForEssl is called with, which decide which block type
|
||||||
|
// names the emitted ESSL spells. Empty for every program without a tessellation or
|
||||||
|
// geometry stage that declares one block name in both directions, i.e. for all but a
|
||||||
|
// handful. The maps rather than what they were derived from: they ARE the pass's
|
||||||
|
// arguments, so they are exactly as fine as its behaviour and no finer.
|
||||||
|
const std::map<String, String>* inputBlockRenames = nullptr;
|
||||||
|
const std::map<String, String>* outputBlockRenames = nullptr;
|
||||||
|
|
||||||
|
// The top of the reserved storage-block window atomic-counter blocks are moved into
|
||||||
|
// (`top - N` for GL binding N). Derived from the driver's
|
||||||
|
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, so it differs per driver, and it is PRINTED
|
||||||
|
// INTO the emitted ESSL as a layout(binding=) qualifier - which makes it key material,
|
||||||
|
// not just a caller's bookkeeping.
|
||||||
|
Int atomicCounterEsslBindingTop = -1;
|
||||||
|
|
||||||
|
// --- SPIRV-Cross options ---
|
||||||
|
Uint esslVersion = 300;
|
||||||
|
|
||||||
|
Bool enableSpirvValidation = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
TranslationCacheKey BuildEsslTranslationKey(const EsslTranslationKeyInputs& inputs);
|
||||||
|
SizeT EsslTranslationResultBytes(const EsslTranslationResult& result);
|
||||||
|
|
||||||
|
BoundedTranslationCache<EsslTranslationResult>& GetEsslTranslationCache();
|
||||||
|
|
||||||
|
// Drops L1c and L2 (L1 lives in MG_State and has its own
|
||||||
|
// ClearProgramTranslationCache). Called from the same teardown that resets the
|
||||||
|
// glslang prewarm latch: nothing here holds a glslang object, so this is RSS
|
||||||
|
// hygiene rather than a correctness requirement.
|
||||||
|
void ClearShaderTranslationCaches();
|
||||||
|
|
||||||
|
// One MGLOG_D line per level. Called at teardown and cheap enough to call from
|
||||||
|
// a test.
|
||||||
|
void LogShaderTranslationCacheStats();
|
||||||
|
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
||||||
@@ -14,6 +14,35 @@ namespace MobileGL {
|
|||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
|
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
|
||||||
|
// 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
|
||||||
|
// buffer, and the trailing number is the only place the GL binding survives.
|
||||||
|
inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock";
|
||||||
|
|
||||||
|
// Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the
|
||||||
|
// shading language's gl_MaxAtomicCounter* constants to report the same numbers
|
||||||
|
// (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly).
|
||||||
|
// They used to be two unreconciled tables: BuildTBuiltInResource compiled against one
|
||||||
|
// binding and glGetIntegerv advertised thirty-six.
|
||||||
|
//
|
||||||
|
// The binding count is what the backends can actually serve. glslang lowers every
|
||||||
|
// atomic_uint onto a storage block, so one counter BUFFER costs one of the ES
|
||||||
|
// driver's shader-storage binding points, and DirectGLES reserves this many at the
|
||||||
|
// top of that range (see AtomicCounterEsslBindingTop in the DirectGLES managers).
|
||||||
|
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_BINDINGS = 8;
|
||||||
|
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, in basic machine units. Independent of the
|
||||||
|
// counter COUNTS below - it bounds the byte offset a counter may be declared at, and
|
||||||
|
// the conformance suite declares counters well past the eighth one (offsets 32 and
|
||||||
|
// 128 in a two-counter buffer). KHR-GL44.multi_bind splits it evenly across every
|
||||||
|
// advertised binding point and binds them all in one glBindBuffersRange, so it must
|
||||||
|
// stay a multiple of, and comfortably larger than, four times the binding count.
|
||||||
|
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_SIZE = 16384;
|
||||||
|
// GL_MAX_{FRAGMENT,COMPUTE,COMBINED}_ATOMIC_COUNTER_BUFFERS and the matching
|
||||||
|
// _ATOMIC_COUNTERS. Eight is the GL 4.6 core minimum for the compute stage
|
||||||
|
// (table 23.45) and every other stage this implementation serves counters on.
|
||||||
|
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
|
||||||
|
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
|
||||||
|
|
||||||
struct EmptyType {};
|
struct EmptyType {};
|
||||||
|
|
||||||
|
|||||||
@@ -47,12 +47,18 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
|
// True when a packed internal format has REDUNDANT encodings, so decoding a texel and
|
||||||
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
|
// re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can
|
||||||
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
|
// be lowered with the mantissas shifted up to match, and the spec's encoder always emits the
|
||||||
// canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32
|
// canonical form, so no readback that goes through a decode cycle can return the stored words.
|
||||||
// bit-exactly, so a GPU readback can answer for them.
|
|
||||||
//
|
//
|
||||||
// This is what decides whether the CPU shadow has to stay authoritative for a format: a
|
// Read this as "a FINITE value re-encodes to different bits", and nothing wider. This comment
|
||||||
// readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no
|
// used to assert that RGB10_A2, RGB10_A2UI and R11F_G11F_B10F "round-trip through float32
|
||||||
// matter how well behaved the driver is.
|
// bit-exactly, so a GPU readback can answer for them", and that is false for
|
||||||
|
// R11F_G11F_B10F: a field whose 5-bit exponent is all ones is an Inf or a NaN, and a NaN's
|
||||||
|
// payload does not survive the trip (EncodeFloatToUnsignedSmallFloat re-encodes every NaN as
|
||||||
|
// the canonical payload 1). glCopyImageSubData from an RGB9_E5 source produces exactly such a
|
||||||
|
// word in the blue field on every texel, because the source's shared-exponent field is all
|
||||||
|
// ones. The bit-exact answer for all four formats is the raw-word route,
|
||||||
|
// DirectGLES::ReadPackedLevelWordsViaScratch; this predicate only picks which of the older
|
||||||
|
// fallbacks to prefer when that route is unavailable.
|
||||||
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
|
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
|
||||||
|
|
||||||
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
|
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
|
||||||
|
|||||||
@@ -31,32 +31,41 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
break;
|
break;
|
||||||
|
// The two render-target bits reach EVERY signed-normalized format, one-, two- and
|
||||||
|
// four-channel included. They used to be granted to GL_RGB16_SNORM alone, which left the
|
||||||
|
// other seven with no colour-renderable fallback at all on a driver without
|
||||||
|
// EXT_render_snorm: an R8_SNORM or R16_SNORM attachment (what KHR-GL4x.texture_swizzle
|
||||||
|
// renders into for every SNORM source format) got no substitute, so the ES framebuffer was
|
||||||
|
// incomplete, the draw landed nowhere and the readback fell through to the never-written
|
||||||
|
// CPU shadow.
|
||||||
case GL_RGB16_SNORM:
|
case GL_RGB16_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case GL_RGBA16_SNORM:
|
case GL_RGBA16_SNORM:
|
||||||
case GL_RG16_SNORM:
|
case GL_RG16_SNORM:
|
||||||
case GL_R16_SNORM:
|
case GL_R16_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
break;
|
break;
|
||||||
case GL_RGBA8_SNORM:
|
case GL_RGBA8_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||||
break;
|
break;
|
||||||
case GL_RGB8_SNORM:
|
case GL_RGB8_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||||
break;
|
break;
|
||||||
case GL_RG8_SNORM:
|
case GL_RG8_SNORM:
|
||||||
case GL_R8_SNORM:
|
case GL_R8_SNORM:
|
||||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||||
|
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||||
break;
|
break;
|
||||||
// The rest of the three-channel formats no real ES driver renders to. They have no
|
// The rest of the three-channel formats no real ES driver renders to. They have no
|
||||||
// other fallback: none of the driver/forced option bits names them, so before the
|
// other fallback: none of the driver/forced option bits names them, so before the
|
||||||
@@ -113,9 +122,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
return {GL_RGBA16F, GL_RGBA, GL_FLOAT};
|
return {GL_RGBA16F, GL_RGBA, GL_FLOAT};
|
||||||
case GL_RGB16_SNORM:
|
case GL_RGB16_SNORM:
|
||||||
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
|
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
|
||||||
// signed-normalized encoding whenever the driver can render to it.
|
// signed-normalized encoding whenever the driver can render to it - and when it
|
||||||
|
// cannot, widen to the 32-bit float, which is the only renderable storage that
|
||||||
|
// still holds all 65535 channel values exactly. GL_RGBA16F here handed -23451/32767
|
||||||
|
// back as -23457, six times the +/-1-step window KHR-GL4x.texture_swizzle allows.
|
||||||
return (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
return (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||||
? ThreeChannelWidening{GL_RGBA16F, GL_RGBA, GL_FLOAT}
|
? ThreeChannelWidening{GL_RGBA32F, GL_RGBA, GL_FLOAT}
|
||||||
: ThreeChannelWidening{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT};
|
: ThreeChannelWidening{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT};
|
||||||
// Unsigned-normalized 16-bit (and the legacy 10/12-bit formats stored as RGB16):
|
// Unsigned-normalized 16-bit (and the legacy 10/12-bit formats stored as RGB16):
|
||||||
// GL_RGB32F is a legal ES texture format but is not colour-renderable either.
|
// GL_RGB32F is a legal ES texture format but is not colour-renderable either.
|
||||||
@@ -203,7 +215,17 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
|
// NoSnorm16RenderTarget outranks the other two 16-bit fallbacks on purpose: it is the
|
||||||
|
// only one whose substitute has to be EXACT, so it picks the 32-bit float rather than
|
||||||
|
// the half the driver/ANGLE fallbacks settle for. The capability probe folds the
|
||||||
|
// driver options and the render-target options into one set while the runtime storage
|
||||||
|
// choice can see the render-target bit alone (GetRuntimeFallbackNormalizeOptions), so
|
||||||
|
// the two would disagree on the storage format without a fixed precedence.
|
||||||
case GL_RGBA16_SNORM:
|
case GL_RGBA16_SNORM:
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
|
*outInternalFormat = GL_RGBA32F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||||
*outInternalFormat = GL_RGBA16F;
|
*outInternalFormat = GL_RGBA16F;
|
||||||
@@ -212,6 +234,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RGB16_SNORM:
|
case GL_RGB16_SNORM:
|
||||||
|
// The three-channel widening below replaces this whenever the target has to stay
|
||||||
|
// renderable; GL_RGB32F keeps the precision for the targets that do not.
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
|
*outInternalFormat = GL_RGB32F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||||
@@ -221,6 +249,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RG16_SNORM:
|
case GL_RG16_SNORM:
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
|
*outInternalFormat = GL_RG32F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||||
*outInternalFormat = GL_RG16F;
|
*outInternalFormat = GL_RG16F;
|
||||||
@@ -229,6 +261,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_R16_SNORM:
|
case GL_R16_SNORM:
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
|
*outInternalFormat = GL_R32F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||||
*outInternalFormat = GL_R16F;
|
*outInternalFormat = GL_R16F;
|
||||||
@@ -236,30 +272,36 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
|
// 8-bit SNORM: the half float already IS exact here, so the render-target bit lands on
|
||||||
|
// the same storage the other two 8-bit fallbacks pick.
|
||||||
case GL_RGBA8_SNORM:
|
case GL_RGBA8_SNORM:
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outInternalFormat = GL_RGBA16F;
|
*outInternalFormat = GL_RGBA16F;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RGB8_SNORM:
|
case GL_RGB8_SNORM:
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outInternalFormat = GL_RGB16F;
|
*outInternalFormat = GL_RGB16F;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_RG8_SNORM:
|
case GL_RG8_SNORM:
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outInternalFormat = GL_RG16F;
|
*outInternalFormat = GL_RG16F;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
*outInternalFormat = internalFormat;
|
*outInternalFormat = internalFormat;
|
||||||
break;
|
break;
|
||||||
case GL_R8_SNORM:
|
case GL_R8_SNORM:
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outInternalFormat = GL_R16F;
|
*outInternalFormat = GL_R16F;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -270,10 +312,30 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
// per-channel precision (extra precision stays inside the CTS comparison epsilon, which is
|
// per-channel precision (extra precision stays inside the CTS comparison epsilon, which is
|
||||||
// derived from the requested format's bit widths). The upload (format, type) below matches
|
// derived from the requested format's bit widths). The upload (format, type) below matches
|
||||||
// the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays).
|
// the canonical shadow layout in PixelStoreProcessor (UNorm8 / UNorm16 component arrays).
|
||||||
|
//
|
||||||
|
// The <=8-bit ones land on the 8-bit-per-channel storage that layout ALREADY is, rather
|
||||||
|
// than on the narrower GL_RGB565/GL_RGBA4 they nominally fit in. Storing them narrower
|
||||||
|
// made the driver requantize the UNorm8 shadow bytes on every upload, and that step is
|
||||||
|
// exact only by luck: 5-bit value 2 encodes as UNorm8 16, and 16/255*31 = 1.945 sits
|
||||||
|
// astride the 5-bit boundary, so a driver that truncates hands back 1 (all twelve
|
||||||
|
// KHR-GL43.copy_image.functional rgb4->rgb4 cases fail on Mali, at verify()'s FIRST
|
||||||
|
// check - a plain glTexImage/glGetTexImage round trip with no copy involved). The
|
||||||
|
// 8-bit store removes the requantization entirely; the client word round-trips
|
||||||
|
// exactly, because encoding an n-bit field to UNorm8 with rounding and back is the
|
||||||
|
// identity for every n <= 8. It is also what DirectVulkan has always done with them
|
||||||
|
// (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats
|
||||||
|
// to R8G8B8A8_UNORM), so the two backends now agree here.
|
||||||
|
//
|
||||||
|
// Only the DESKTOP-ONLY formats move. GL_RGBA4 and GL_RGB5_A1 are ES formats an
|
||||||
|
// application can legitimately ask for - the same normalization picks the storage for
|
||||||
|
// glRenderbufferStorage - so widening them would be a memory decision, not a
|
||||||
|
// correctness one. Nothing about the REPORTED precision moves either way:
|
||||||
|
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on the
|
||||||
|
// requested format, not on the ES storage.
|
||||||
case GL_R3_G3_B2:
|
case GL_R3_G3_B2:
|
||||||
case GL_RGB4:
|
case GL_RGB4:
|
||||||
case GL_RGB5:
|
case GL_RGB5:
|
||||||
*outInternalFormat = GL_RGB565;
|
*outInternalFormat = GL_RGB8;
|
||||||
break;
|
break;
|
||||||
case GL_RGB10:
|
case GL_RGB10:
|
||||||
case GL_RGB12:
|
case GL_RGB12:
|
||||||
@@ -283,7 +345,7 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
: GL_RGB16;
|
: GL_RGB16;
|
||||||
break;
|
break;
|
||||||
case GL_RGBA2:
|
case GL_RGBA2:
|
||||||
*outInternalFormat = GL_RGBA4;
|
*outInternalFormat = GL_RGBA8;
|
||||||
break;
|
break;
|
||||||
case GL_RGBA12:
|
case GL_RGBA12:
|
||||||
*outInternalFormat =
|
*outInternalFormat =
|
||||||
@@ -513,7 +575,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||||
(internalFormat == GL_RGB16_SNORM &&
|
(internalFormat == GL_RGB16_SNORM &&
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm)) ||
|
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm)) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)) {
|
||||||
*outType = GL_FLOAT;
|
*outType = GL_FLOAT;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
@@ -523,7 +586,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
case GL_RGB8_SNORM:
|
case GL_RGB8_SNORM:
|
||||||
case GL_RG8_SNORM:
|
case GL_RG8_SNORM:
|
||||||
case GL_R8_SNORM:
|
case GL_R8_SNORM:
|
||||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outType = GL_FLOAT;
|
*outType = GL_FLOAT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -531,7 +595,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
|||||||
break;
|
break;
|
||||||
case GL_RGBA8_SNORM:
|
case GL_RGBA8_SNORM:
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||||
|
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||||
*outType = GL_FLOAT;
|
*outType = GL_FLOAT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,21 @@ namespace MobileGL {
|
|||||||
// three-channel client data with an alpha of 1.0, and sampling/readback has to hide
|
// three-channel client data with an alpha of 1.0, and sampling/readback has to hide
|
||||||
// the added alpha again (BackendTextureFormatAddsAlpha).
|
// the added alpha again (BackendTextureFormatAddsAlpha).
|
||||||
NoThreeChannelRenderTarget = 1 << 7,
|
NoThreeChannelRenderTarget = 1 << 7,
|
||||||
// Pairs with the bit above: the widened four-channel format has to stay renderable AND
|
// A 16-bit signed-normalized image has to back a colour attachment, and the driver cannot
|
||||||
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and
|
// render to the signed-normalized encoding itself: that needs both EXT_texture_norm16 and
|
||||||
// EXT_render_snorm. Without them the only renderable widening left is a half float, whose
|
// EXT_render_snorm, and without either one an R16_SNORM / RG16_SNORM / RGB16_SNORM /
|
||||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
// RGBA16_SNORM attachment is texture-only, so the framebuffer is never complete and the
|
||||||
|
// draw silently lands nowhere. The substitute is a 32-bit float, NOT the half float the
|
||||||
|
// other SNORM fallbacks use: a half's 11-bit mantissa cannot represent a 16-bit SNORM
|
||||||
|
// channel exactly - its spacing just below 1.0 is 2^-11, some 16 SNORM steps, so
|
||||||
|
// -23451/32767 comes back as -23457 - while a 32-bit float round-trips every one of the
|
||||||
|
// 65535 channel values bit for bit.
|
||||||
NoSnorm16RenderTarget = 1 << 8,
|
NoSnorm16RenderTarget = 1 << 8,
|
||||||
|
// The 8-bit twin of the bit above: without EXT_render_snorm an R8_SNORM / RG8_SNORM /
|
||||||
|
// RGB8_SNORM / RGBA8_SNORM colour attachment is not renderable either. Here a half float
|
||||||
|
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
||||||
|
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
||||||
|
NoSnorm8RenderTarget = 1 << 9,
|
||||||
None = 0,
|
None = 0,
|
||||||
};
|
};
|
||||||
namespace MG_Util::TextureFormatProcessor {
|
namespace MG_Util::TextureFormatProcessor {
|
||||||
|
|||||||
@@ -57,6 +57,39 @@ def mem_available_kb(serial):
|
|||||||
return int(m.group(1)) if m else None
|
return int(m.group(1)) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def core_max_frequencies(serial):
|
||||||
|
r = adb(serial, "shell",
|
||||||
|
"for d in /sys/devices/system/cpu/cpu*/cpufreq; do "
|
||||||
|
"cat $d/cpuinfo_max_freq 2>/dev/null || echo 0; done", timeout=30)
|
||||||
|
freqs = [int(x) for x in re.findall(r"\d+", r.stdout or "")]
|
||||||
|
return freqs if freqs and max(freqs) > 0 else []
|
||||||
|
|
||||||
|
|
||||||
|
def derive_cpu_mask(serial, mode):
|
||||||
|
"""taskset mask for `mode`: 'prime' (fastest core only) or 'fast' (fast cluster).
|
||||||
|
|
||||||
|
Measured on the Mali G925, one texture_swizzle smoke case, two rounds in opposite
|
||||||
|
orders: unpinned 13.50/13.74 s, fast cluster 10.22/9.89 s, prime core 6.82/4.38 s.
|
||||||
|
Peak thread count was 11 in every configuration, so pinning does NOT cost MobileGL
|
||||||
|
any of its compile-pool parallelism - the unpinned run is simply losing to the
|
||||||
|
scheduler parking a CPU-bound load on the little cluster.
|
||||||
|
"""
|
||||||
|
freqs = core_max_frequencies(serial)
|
||||||
|
if not freqs:
|
||||||
|
return None
|
||||||
|
top = max(freqs)
|
||||||
|
if mode == "prime":
|
||||||
|
# The single fastest core. Fastest of the three in measurement, though with the
|
||||||
|
# widest spread, which is why it is opt-in rather than the default.
|
||||||
|
return f"{1 << freqs.index(top):x}"
|
||||||
|
cutoff = top * 0.7
|
||||||
|
mask = 0
|
||||||
|
for cpu, freq in enumerate(freqs):
|
||||||
|
if freq >= cutoff:
|
||||||
|
mask |= 1 << cpu
|
||||||
|
return f"{mask:x}" if mask else None
|
||||||
|
|
||||||
|
|
||||||
def completed_cases(qpa_path):
|
def completed_cases(qpa_path):
|
||||||
"""Return (finished_case_names, last_started_case_or_None).
|
"""Return (finished_case_names, last_started_case_or_None).
|
||||||
|
|
||||||
@@ -99,6 +132,20 @@ def main():
|
|||||||
# the contradiction instead of papering over it.
|
# the contradiction instead of papering over it.
|
||||||
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
|
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
|
||||||
help="--deqp-gl-config-name value (empty string to leave it unset)")
|
help="--deqp-gl-config-name value (empty string to leave it unset)")
|
||||||
|
# A CTS run is CPU-bound (measured: cpu/wall = 93% on a texture_swizzle smoke case,
|
||||||
|
# which spends its time in glslang and spirv-tools, not in the driver), and Android's
|
||||||
|
# scheduler parks that load on the little cluster. Measured on the Mali G925 device,
|
||||||
|
# one smoke case: unpinned 16 s, cores 4-7 6 s, core 7 alone 5 s (unpinned re-run 16 s,
|
||||||
|
# so this is not drift). Pinning is worth 2.7-3.2x, and a NARROWER mask was faster,
|
||||||
|
# not slower - the compile pool's parallelism does not pay for the cross-core migration
|
||||||
|
# once the translation cache absorbs most of the compiles. "auto" keeps every core
|
||||||
|
# within 70% of the fastest, which drops the little cluster; that leaves room to run
|
||||||
|
# shards on separate cores, which is worth more than the last 20%.
|
||||||
|
ap.add_argument("--cpu-mask", default="fast",
|
||||||
|
help="CPU affinity for glcts: 'fast' (every core within 70%% of the "
|
||||||
|
"fastest, i.e. the big cluster), 'prime' (the single fastest core, "
|
||||||
|
"quickest measured but with the widest spread), 'none' (leave "
|
||||||
|
"affinity alone), or an explicit taskset hex mask")
|
||||||
ap.add_argument("--max-rounds", type=int, default=4000)
|
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||||
ap.add_argument("--max-empty-streak", type=int, default=64,
|
ap.add_argument("--max-empty-streak", type=int, default=64,
|
||||||
help="abort after this many consecutive chunks that produce no log at all")
|
help="abort after this many consecutive chunks that produce no log at all")
|
||||||
@@ -128,6 +175,16 @@ def main():
|
|||||||
total = len(remaining)
|
total = len(remaining)
|
||||||
print(f"[run_cts] {args.backend} on {args.serial}: {total} cases")
|
print(f"[run_cts] {args.backend} on {args.serial}: {total} cases")
|
||||||
|
|
||||||
|
cpu_mask = None
|
||||||
|
if args.cpu_mask in ("fast", "prime", "auto"): # auto kept as an alias for fast
|
||||||
|
cpu_mask = derive_cpu_mask(args.serial, "prime" if args.cpu_mask == "prime" else "fast")
|
||||||
|
if cpu_mask is None:
|
||||||
|
print("[run_cts] could not read cpufreq; leaving affinity alone", file=sys.stderr)
|
||||||
|
elif args.cpu_mask != "none":
|
||||||
|
cpu_mask = args.cpu_mask
|
||||||
|
if cpu_mask:
|
||||||
|
print(f"[run_cts] pinning glcts to CPU mask 0x{cpu_mask} (--cpu-mask {args.cpu_mask})")
|
||||||
|
|
||||||
crashed = []
|
crashed = []
|
||||||
hung = []
|
hung = []
|
||||||
done = set()
|
done = set()
|
||||||
@@ -167,12 +224,13 @@ def main():
|
|||||||
config_flag = (
|
config_flag = (
|
||||||
f"--deqp-gl-config-name={args.gl_config_name} " if args.gl_config_name else ""
|
f"--deqp-gl-config-name={args.gl_config_name} " if args.gl_config_name else ""
|
||||||
)
|
)
|
||||||
|
taskset_prefix = f"taskset {cpu_mask} " if cpu_mask else ""
|
||||||
# The trailing sync makes the qpa durable: a hard GPU hang reboots the
|
# The trailing sync makes the qpa durable: a hard GPU hang reboots the
|
||||||
# device, and f2fs rolls back unsynced writes, silently eating the log.
|
# device, and f2fs rolls back unsynced writes, silently eating the log.
|
||||||
cmd = (
|
cmd = (
|
||||||
f"cd {args.device_dir} && "
|
f"cd {args.device_dir} && "
|
||||||
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
|
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
|
||||||
f"./glcts --deqp-caselist-file={dev_list} "
|
f"{taskset_prefix}./glcts --deqp-caselist-file={dev_list} "
|
||||||
f"--deqp-surface-type={args.surface} "
|
f"--deqp-surface-type={args.surface} "
|
||||||
f"--deqp-surface-width={args.surface_size} "
|
f"--deqp-surface-width={args.surface_size} "
|
||||||
f"--deqp-surface-height={args.surface_size} "
|
f"--deqp-surface-height={args.surface_size} "
|
||||||
|
|||||||
Reference in New Issue
Block a user